Merge remote-tracking branch 'upstream/master'

This commit is contained in:
samturner3
2024-08-24 16:18:17 +10:00
159 changed files with 9157 additions and 6235 deletions

View File

@@ -1,17 +1,25 @@
# DB Type: sqlite, mysql, postgresql, cockroachdb
# DB Type: sqlite, mysql, postgres, cockroachdb
# Default if not set: sqlite
#SPOOLMAN_DB_TYPE=sqlite
# DB Setup, if not using sqlite
# Database hostname
#SPOOLMAN_DB_HOST=
# Database port
#SPOOLMAN_DB_PORT=
# Database name
#SPOOLMAN_DB_NAME=
# Database login username
#SPOOLMAN_DB_USERNAME=
# Path of file which contains the database password. Can be used instead of SPOOLMAN_DB_PASSWORD if desired.
#SPOOLMAN_DB_PASSWORD_FILE=
# Database login password
#SPOOLMAN_DB_PASSWORD=
# Query parameters for the database connection, e.g. set to `unix_socket=/path/to/mysql.sock` to connect using a MySQL socket.
#SPOOLMAN_DB_QUERY=
# Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL
# Logs will only be reported if the level is higher than the level set here
# Default if not set: INFO
#SPOOLMAN_LOGGING_LEVEL=INFO
@@ -34,3 +42,33 @@
# Host and port to listen on
SPOOLMAN_HOST=0.0.0.0
SPOOLMAN_PORT=7912
# Change base path
# Set this if you want to host Spoolman at a sub-path
# If you want the root to be e.g. myhost.com/spoolman
# Then set this to /spoolman
#SPOOLMAN_BASE_PATH=
# Enable Collect Prometheus metrics at database
# Default: FALSE
#SPOOLMAN_METRICS_ENABLED=TRUE
# Collect items (filaments, materials, etc.) from an external database
# Set this to a URL of an external database. Set to an empty string to disable
# Default: https://donkie.github.io/SpoolmanDB/
#EXTERNAL_DB_URL=https://myhost.com/spoolmandb/
# Sync interval in seconds, set to 0 to disable automatic sync. It will only sync on start-up then.
# Default: 3600
#EXTERNAL_DB_SYNC_INTERVAL=3600
# Enable debug mode
# If enabled, the client will accept requests from any host
# This can be useful when developing, but is also a security risk
# Default: FALSE
#SPOOLMAN_DEBUG_MODE=TRUE
# UID and GID of the user in the docker container
# These only make sense if you are running Spoolman in a docker container
# Default if not set: 1000
#PUID=1000
#PGID=1000

View File

@@ -23,16 +23,15 @@ A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Spoolman Host (please complete the following information):**
- Installation type: [Docker or Standalone]
- Installed version: [e.g. 0.17.0, can be found at the bottom of your spoolman website if you don't know]
- OS and Distro: [e.g. Ubuntu, Raspbian]
- Database type: [SQLite, PostgreSQL, etc]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Desktop (please complete the following information):**
- OS: [e.g. Windows, Mac, iOS, Android, ...]
- Browser [e.g. Chrome, Firefox, Safari, ...]
**Additional context**
Add any other context about the problem here.

View File

@@ -57,7 +57,7 @@ jobs:
- name: Export requirements
run: |
pip install pdm
pdm export -o requirements.txt --without-hashes > requirements.txt
pdm export -o requirements.txt --without-hashes
- name: Install client dependencies
run: |
@@ -219,7 +219,7 @@ jobs:
docker load --input /tmp/spoolman-tester-image/spoolman-tester.tar
- name: Perform integration tests
uses: isbang/compose-action@v1.4.1
uses: hoverkraft-tech/compose-action@v2.0.1
with:
compose-file: "./tests_integration/docker-compose-${{ matrix.dbtype }}.yml"
up-flags: "--abort-on-container-exit"

1
.gitignore vendored
View File

@@ -203,3 +203,4 @@ $RECYCLE.BIN/
data/
.pdm-python
.history

View File

@@ -9,16 +9,16 @@ repos:
- id: check-case-conflict
- id: check-merge-conflict
- repo: https://github.com/psf/black
rev: 23.3.0
rev: 24.4.2
hooks:
- id: black
args: ["--check"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.286
rev: v0.4.4
hooks:
- id: ruff
args: ["--target-version", "py39"]
- repo: https://github.com/pdm-project/pdm
rev: 2.7.4
rev: 2.17.3
hooks:
- id: pdm-lock-check

View File

@@ -1,6 +1,17 @@
FROM python:3.11-alpine as python-builder
FROM python:3.12-bookworm as python-builder
RUN apk add --no-cache g++ python3-dev libpq-dev libstdc++ shadow
# Install dependencies
RUN apt-get update && apt-get install -y \
g++ \
python3-dev \
libpq-dev \
libffi-dev \
python3-pip \
python3-setuptools \
python3-wheel \
python3-pdm \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Add local user so we don't run as root
RUN groupmod -g 1000 users \
@@ -9,10 +20,6 @@ RUN groupmod -g 1000 users \
ENV PATH="/home/app/.local/bin:${PATH}"
# Install PDM
RUN pip install pip setuptools wheel\
&& pip install pdm
# Copy and install dependencies
COPY --chown=app:app pyproject.toml /home/app/spoolman/
COPY --chown=app:app pdm.lock /home/app/spoolman/
@@ -25,13 +32,28 @@ COPY --chown=app:app spoolman /home/app/spoolman/spoolman
COPY --chown=app:app alembic.ini /home/app/spoolman/
COPY --chown=app:app README.md /home/app/spoolman/
FROM python:3.11-alpine as python-runner
FROM python:3.12-bookworm as python-runner
LABEL org.opencontainers.image.source=https://github.com/Donkie/Spoolman
LABEL org.opencontainers.image.description="Keep track of your inventory of 3D-printer filament spools."
LABEL org.opencontainers.image.licenses=MIT
RUN apk add --no-cache libstdc++ su-exec shadow
# Install latest su-exec
RUN set -ex; \
\
curl -o /usr/local/bin/su-exec.c https://raw.githubusercontent.com/ncopa/su-exec/master/su-exec.c; \
\
fetch_deps='gcc libc-dev'; \
apt-get update; \
apt-get install -y --no-install-recommends $fetch_deps; \
rm -rf /var/lib/apt/lists/*; \
gcc -Wall \
/usr/local/bin/su-exec.c -o/usr/local/bin/su-exec; \
chown root:root /usr/local/bin/su-exec; \
chmod 0755 /usr/local/bin/su-exec; \
rm /usr/local/bin/su-exec.c; \
\
apt-get purge -y --auto-remove $fetch_deps
# Add local user so we don't run as root
RUN groupmod -g 1000 users \

180
README.md
View File

@@ -4,51 +4,82 @@
<img alt="Icon of a filament spool" src="https://github.com/Donkie/Spoolman/assets/2332094/3c120b3a-1422-42f6-a16b-8d5a07c33000">
</picture>
<br/><br/>
<br/>
_Keep track of your inventory of 3D-printer filament spools._
Spoolman is a web service that helps you keep track of your filament spools and how they are being used.
Spoolman is a self-hosted web service designed to help you efficiently manage your 3D printer filament spools and monitor their usage. It acts as a centralized database that seamlessly integrates with popular 3D printing software like [OctoPrint](https://octoprint.org/) and [Klipper](https://www.klipper3d.org/)/[Moonraker](https://moonraker.readthedocs.io/en/latest/). When connected, it automatically updates spool weights as printing progresses, giving you real-time insights into filament usage.
It acts as a database, where other printer software such as Octoprint and Moonraker can interact with to have a centralized place for spool information.
For example, if used together with Moonraker, your spool weight will automatically be reduced as your print is progressing.
### Features
* **Filament Management**: Keep comprehensive records of filament types, manufacturers, and individual spools.
* **API Integration**: The [REST API](https://donkie.github.io/Spoolman/) allows easy integration with other software, facilitating automated workflows and data exchange.
* **Real-Time Updates**: Stay informed with live spool updates through Websockets, providing immediate feedback during printing operations.
* **Web-Based Client**: Spoolman includes a built-in web client that lets you manage data effortlessly:
* View, create, edit, and delete filament data.
* Add custom fields to tailor information to your specific needs.
* Print labels with QR codes for easy spool identification and tracking.
* Contribute to its translation into 18 languages via [Weblate](https://hosted.weblate.org/projects/spoolman/).
* **Database Support**: SQLite, PostgreSQL, MySQL, and CockroachDB.
* **Multi-Printer Management**: Handles spool updates from several printers simultaneously.
* **Advanced Monitoring**: Integrate with [Prometheus](https://prometheus.io/) for detailed historical analysis of filament usage, helping you track and optimize your printing processes.
It exposes a HTTP API which services can interact with. See the [OpenAPI description](https://donkie.github.io/Spoolman/) for more information.
## Client
Spoolman includes a web-client that lets you directly manipulate all the data. It also has a few additional nice features such as label printing.
**Integrations:**
* [Moonraker](https://moonraker.readthedocs.io/en/latest/configuration/#spoolman) and most front-ends (Fluidd, KlipperScreen, Mainsail, ...)
* [OctoPrint](https://github.com/mdziekon/octoprint-spoolman)
* [OctoEverywhere](https://octoeverywhere.com/spoolman?source=github_spoolman)
* [Homeassistant](https://github.com/Disane87/spoolman-homeassistant)
**Web client preview:**
![image](https://github.com/Donkie/Spoolman/assets/2332094/33928d5e-440f-4445-aca9-456c4370ad0d)
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://hosted.weblate.org/widget/spoolman/287x66-black.png">
<source media="(prefers-color-scheme: light)" srcset="https://hosted.weblate.org/widget/spoolman/287x66-white.png">
<img alt="Icon of a filament spool" src="https://hosted.weblate.org/widget/spoolman/287x66-white.png">
</picture>
_The web client is translated by the community using [Weblate](https://hosted.weblate.org/projects/spoolman/)._
## Integration status
Spoolman doesn't depend on any specific printer software, but to make the most out of it, you should use it together with a frontend that supports it.
It is currently only supported in the Klipper ecosystem, with official support for the major frontends. Support for other ecosystems like Octoprint is ongoing.
* ✔️ Moonraker - See the [Moonraker Documentation](https://moonraker.readthedocs.io/en/latest/configuration/#spoolman)
* ✔️ Fluidd
* ✔️ KlipperScreen
* ✔️ Mainsail
* ✖️ Octoprint - A plugin is in progress: [OctoPrint-Spoolman](https://github.com/mkevenaar/OctoPrint-Spoolman)
* ✔️ Home Assistant integration (https://github.com/Disane87/spoolman-homeassistant)
## Installation
Spoolman can interact with any of the following databases: SQLite, PostgreSQL, MySQL, MariaDB, CockroachDB.
By default, SQLite is used which is a simple no-install database solution that saves to a single .db file located in the server's user directory.
Spoolman can be installed in two ways, either using **Docker** or **standalone** on your machine.
* The Docker method can be used on any OS which supports Docker.
* The standalone method requires a Debian (e.g. Ubuntu, Raspberry Pi OS) or Arch-based Linux distribution.
* If you know what you're doing you can also install it standalone for any OS such as Windows, but there are no provided scripts for automated installation, follow the "Install from Source" instructions below.
Spoolman can be installed in two ways, either standalone on your machine or using Docker. If you already have Docker installed, it's recommended to use that.
Docker is the recommended installation method since it is more reliable and portable.
### Standalone
This installation guide assumes you are using a Debian-based Linux distribution such as Ubuntu or Raspberry Pi OS. If you are using another distribution, please look inside the bash scripts to see what commands are being run and adapt them to your distribution.
Spoolman comes with a built-in SQLite database that is used by default and will suit most users needs. The data is stored in a single .db file in the server's user directory. You can optionally instead use a PostgreSQL, MySQL or CockroachDB database.
Copy-paste the entire below command and run it on your machine to install Spoolman.
### Docker Install
#### First install
Docker is a platform for developing, shipping, and running applications in "containers". Containers are lightweight, portable, and self-contained environments that can run on any machine with Docker installed. The Spoolman docker image contains everything it needs to run, so you don't need to worry about for example what Python version you have installed on your local machine.
To install Docker on your machine, follow the instructions for your operating system on the [Docker website](https://docs.docker.com/engine/install/). Docker also includes the docker-compose tool which lets you configure the container deployment in a simple yaml file, without having to remember all the command line options. Note: older versions of docker-compose require you to have a dash (`-`) in the following commands, like `docker-compose` instead of `docker compose`.
Here is a sample docker-compose config to get you started. Create a folder called `spoolman` in your home directory, CD in to it, copy-paste the below sample into a file called `docker-compose.yml` in the new directory and run `docker compose up -d` to start it. If you want to use the SQLite database as in this sample, you must first create a folder called `data` in the same directory as the `docker-compose.yml`, then you should run `chown 1000:1000 data` on it in order to give it the correct permissions for the user inside the docker container.
```yaml
version: '3.8'
services:
spoolman:
image: ghcr.io/donkie/spoolman:latest
restart: unless-stopped
volumes:
# Mount the host machine's ./data directory into the container's /home/app/.local/share/spoolman directory
- type: bind
source: ./data # This is where the data will be stored locally. Could also be set to for example `source: /home/pi/printer_data/spoolman`.
target: /home/app/.local/share/spoolman # Do NOT modify this line
ports:
# Map the host machine's port 7912 to the container's port 8000
- "7912:8000"
environment:
- TZ=Europe/Stockholm # Optional, defaults to UTC
```
Once you have it up and running, you can access the web UI by browsing to `http://server.ip:7912`. *Make sure that the data folder you created now contains a `spoolman.db` file*. If you cannot find this file in your machine, then **your data will be lost** every time you update Spoolman.
Type `docker compose logs` in the terminal to see the server logs.
If you want to modify things like database connection, add environment variables to the `environment:` section of the `docker-compose.yml` like shown in the sample above and then restart the service: `docker compose restart`. See the `.env.example` file for a list of all environment variables you can use.
#### Updating
If a new version of Spoolman has been released, you can update to it by first browsing to the directory where you have the `docker-compose.yml` file and then running `docker compose pull && docker compose up -d`.
### Standalone Install
#### First install
This installation method assumes you are using a Debian-based Linux distribution such as Ubuntu or Raspberry Pi OS. If you are using Arch, or any other distribution, please modify as appropriate.
Make sure your current directory is in your logged in user's home directory. Copy-paste the entire below command and run it on your machine to install the latest release of Spoolman.
```bash
sudo apt-get update && \
sudo apt-get install -y curl jq && \
@@ -56,9 +87,11 @@ mkdir -p ./Spoolman && \
source_url=$(curl -s https://api.github.com/repos/Donkie/Spoolman/releases/latest | jq -r '.assets[] | select(.name == "spoolman.zip").browser_download_url') && \
curl -sSL $source_url -o temp.zip && unzip temp.zip -d ./Spoolman && rm temp.zip && \
cd ./Spoolman && \
bash ./scripts/install_debian.sh
bash ./scripts/install.sh
```
If you want to modify things like database connection, edit the `.env` file in the `Spoolman` folder and restart the service.
#### Updating
Updating Spoolman is quite simple. If you use the default database type, SQLite, it is stored outside of the installation folder (in `~/.local/share/spoolman`), so you will not lose any data by moving to a new installation folder.
@@ -77,72 +110,41 @@ source_url=$(curl -s https://api.github.com/repos/Donkie/Spoolman/releases/lates
curl -sSL $source_url -o temp.zip && unzip temp.zip -d ./Spoolman && rm temp.zip && \
cp Spoolman_old/.env Spoolman/.env && \
cd ./Spoolman && \
bash ./scripts/install_debian.sh && \
bash ./scripts/install.sh && \
rm -rf ../Spoolman_old
```
### Using Docker
You can also run Spoolman using Docker. Docker is a platform for developing, shipping, and running applications in containers. Containers are lightweight, portable, and self-contained environments that can run on any machine with Docker installed.
To install Docker on your machine, follow the instructions for your operating system on the [Docker website](https://docs.docker.com/engine/install/). Docker also includes the docker-compose tool which lets you configure the container deployment in a simple yaml file, without having to remember all the command line options. Note: older versions of docker-compose require you to have a dash (`-`) in the following commands, like `docker-compose` instead of `docker compose`.
Here is a sample docker-compose config to get you started. Copy-paste it into a file called `docker-compose.yml` and run `docker compose up -d` to start it. If you want to use the SQLite database as in this sample, you must first create a folder called `data` in the same directory as the `docker-compose.yml`, then you should run `chown 1000:1000 data` on it in order to give it the correct permissions for the user inside the docker container.
```yaml
version: '3.8'
services:
spoolman:
image: ghcr.io/donkie/spoolman:latest
restart: unless-stopped
volumes:
# Mount the host machine's ./data directory into the container's /home/app/.local/share/spoolman directory
- type: bind
source: ./data # This is where the data will be stored locally. Could also be set to for example `source: /home/pi/printer_data/spoolman`.
target: /home/app/.local/share/spoolman # Do NOT change this line
ports:
# Map the host machine's port 7912 to the container's port 8000
- "7912:8000"
environment:
- TZ=Europe/Stockholm # Optional, defaults to UTC
```
Once you have it up and running, you can access the web UI by browsing to `http://your.ip:7912`. Make sure that the data folder you created now contains a `spoolman.db` file. If you cannot find this file in your machine, then **your data will be lost** every time you update Spoolman.
#### Updating
If a new version of Spoolman has been released, you can update to it by first browsing to the directory where you have the `docker-compose.yml` file and then running `docker compose pull && docker compose up -d`.
### Environment variables
These are either set in the .env file if you use the standalone installation, or in the docker-compose.yml if you use Docker.
If you want to connect with an external database instead, specify the `SPOOLMAN_DB_*` environment variables from the table below.
| Variable | Description |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| SPOOLMAN_DB_TYPE | Type of database, any of: `postgres`, `mysql`, `sqlite`, `cockroachdb` |
| SPOOLMAN_DB_HOST | Database hostname |
| SPOOLMAN_DB_PORT | Database port |
| SPOOLMAN_DB_NAME | Database name |
| SPOOLMAN_DB_USERNAME | Database username |
| SPOOLMAN_DB_PASSWORD_FILE | Path of file which contains the database password. Can be used instead of SPOOLMAN_DB_PASSWORD if desired. |
| SPOOLMAN_DB_PASSWORD | Database password |
| SPOOLMAN_DB_QUERY | Query parameters for the database connection, e.g. set to `unix_socket=/path/to/mysql.sock` to connect using a MySQL socket. |
| SPOOLMAN_LOGGING_LEVEL | Logging level, any of: `CRITICAL`, `ERROR`, `WARNING`, `INFO`, `DEBUG`, defaults to `INFO`. |
| SPOOLMAN_AUTOMATIC_BACKUP | Automatic nightly DB backups for SQLite databases. Enabled by default, set to `FALSE` to disable. |
| PUID | (*docker only*) Set the UID of the user in the docker container. Default is 1000. |
| PGID | (*docker only*) Set the GID of the user in the docker container. Default is 1000. |
| SPOOLMAN_PORT | The port Spoolman should run on (default: 8000) |
| SPOOLMAN_HOST | The hostname/ip Spoolman should bind to (default: 0.0.0.0) |
## Frequently Asked Questions (FAQs)
### QR Code Does not work on HTTP / The page is not served over HTTPS
This is a limitation of the browsers. Browsers require a secure connection to the server to enable HTTPS. This is not a limitation of Spoolman. For more information read this [blog](https://blog.mozilla.org/webrtc/camera-microphone-require-https-in-firefox-68/) from Mozilla.
You can put Spoolman behind a reverse proxy like Caddy or Nginx to enable HTTPS. See for example [this guide](https://caddyserver.com/docs/quick-starts/reverse-proxy) for Caddy.
[OctoEverywhere.com](https://octoeverywhere.com/spoolman?source=github_spoolman_qr) is An easy way to get secure HTTPS access to Spoolman. Remote access with OctoEverywhere is always secure and easier than setting up a reverse proxy + SSL self-signed certificate.
Another option is to put Spoolman behind a reverse proxy like Caddy or Nginx to enable HTTPS. See for example [this guide](https://caddyserver.com/docs/quick-starts/reverse-proxy) for Caddy.
### Can Spoolman be translated into my language?
Yes, head over to [Weblate](https://hosted.weblate.org/projects/spoolman/) to start the Translation
Yes, head over to [Weblate](https://hosted.weblate.org/projects/spoolman/) to start the translation!
## Install from source
**Advanced users only.**
If you want to run the absolute latest version of Spoolman, you can either use the `edge` tagged Docker image, or follow
these steps to install from source. Keep in mind that this is straight from the master branch which may contain unfinished and untested features. You may also not be able to go back to a previous version since the database schema may change.
1. Make sure you have at least NodeJS 20 or higher installed, and Python 3.9 or higher installed.
1. Clone this repo or download the zip source.
2. Inside the `client/` folder:
1. Create a .env file with `VITE_APIURL=/api/v1` in it
2. Run `npm ci`
3. Run `npm run build`
4. Install PDM using `pip install --user pdm`
5. Build the requirements.txt file: `pdm export -o requirements.txt --without-hashes > requirements.txt`
3. Give scripts permissions: `chmod +x ./scripts/*.sh`
6. Run the installer script like the normal install: `./scripts/install.sh`
## Development
### Server/Backend (Python)
The Python backend runs on Python 3.9. It's built on FastAPI for the REST API, and SQLAlchemy to handle the databases.
The Python backend is built using Python 3.9 standards. It's built on FastAPI for the REST API, and SQLAlchemy to handle the databases.
To setup yourself for Python development, do the following:
1. Clone this repo
@@ -162,7 +164,7 @@ pre-commit install
```
#### Integration Testing
The entire REST API is integration tested using an isolated docker container, with all 4 database types that we support (Postgres, MySQL, SQLite and CockroachDB). These integration tests live in `tests_integration/`. They are designed to "use" the REST API in the same way that a client would, and ensures that everything remains consistent between updates. The databases are created as part of the integration testing, so no external database is needed to run them.
The entire backend is integration tested using an isolated docker container, with all 4 database types that we support (Postgres, MySQL, SQLite and CockroachDB). These integration tests live in `tests_integration/`. They are designed to "use" the REST API in the same way that a client would, and ensures that everything remains consistent between updates. The databases are created as part of the integration testing, so no external database is needed to run them.
If you have docker installed, you can run the integration tests using `pdm run itest` for all databases, or e.g. `pdm run itest postgres` for a single database.

View File

@@ -2,11 +2,7 @@
module.exports = {
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"],
parser: "@typescript-eslint/parser",
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
plugins: ["react-refresh"],

View File

@@ -1,93 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
width="235"
height="500"
viewBox="0 0 235 500"
xml:space="preserve"
id="svg5"
sodipodi:docname="spool.svg"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview5"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6588725"
inkscape:cx="132.92161"
inkscape:cy="209.47963"
inkscape:window-width="1350"
inkscape:window-height="1237"
inkscape:window-x="1189"
inkscape:window-y="48"
inkscape:window-maximized="0"
inkscape:current-layer="svg5" />&#10;<desc
id="desc1">Created with Fabric.js 5.3.0</desc>&#10;<defs
id="defs1">&#10;</defs>&#10;&#10;&#10;&#10;&#10;<path
style="stroke:rgb(110, 11, 48);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:rgb(52, 52, 52);fill-rule:nonzero;opacity:1"
vector-effect="non-scaling-stroke"
transform="matrix(0.58757483,0,0,3.9476902,197.13549,250.04675)"
d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z"
stroke-linecap="round"
id="path1"
inkscape:label="rightside" /><path
style="stroke:rgb(110, 11, 48);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:rgb(255, 0, 0);fill-rule:nonzero;opacity:1"
vector-effect="non-scaling-stroke"
transform="matrix(0.38157612,0,0,3.4623174,197.13549,250.04674)"
d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z"
stroke-linecap="round"
id="path2"
inkscape:label="filament_right" /><path
style="stroke:rgb(0, 0, 0);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:rgb(255, 0, 0);fill-rule:nonzero;opacity:1"
vector-effect="non-scaling-stroke"
transform="matrix(2.073642,0,0,3.3577086,117.29471,250.04674)"
d="M -38.50267 -65.24064 L 38.502680000000005 -65.24064 L 38.502680000000005 65.24064 L -38.50267 65.24064 z"
stroke-linecap="round"
id="path3"
inkscape:label="filament_main" /><g
transform="matrix(0.58757483,0,0,3.9476902,37.453957,250.04674)"
id="OvgM38-6CZm9R47a1TDIX"
inkscape:label="fg">&#10;<filter
id="SVGID_1"
y="-0.056899467"
height="1.1137989"
x="-0.056899467"
width="1.271853">&#10; <feGaussianBlur
in="SourceAlpha"
stdDeviation="3"
id="feGaussianBlur3" />&#10; <feOffset
dx="20"
dy="0"
result="oBlur"
id="feOffset3" />&#10; <feFlood
flood-color="rgb(0,0,0)"
flood-opacity="0.67"
id="feFlood3" />&#10; <feComposite
in2="oBlur"
operator="in"
id="feComposite3" />&#10; <feMerge
id="feMerge4">&#10; <feMergeNode
id="feMergeNode3" />&#10; <feMergeNode
in="SourceGraphic"
id="feMergeNode4" />&#10; </feMerge>&#10;</filter>&#10;&#10;<path
style="stroke:rgb(110, 11, 48);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:rgb(52, 52, 52);fill-rule:nonzero;opacity:1;filter:url(#SVGID_1)"
vector-effect="non-scaling-stroke"
transform="translate(8.4959392e-7)"
d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z"
stroke-linecap="round"
id="path4"
inkscape:label="leftside" /><path
style="stroke:rgb(110, 11, 48);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:rgb(17, 17, 17);fill-rule:nonzero;opacity:1"
vector-effect="non-scaling-stroke"
transform="scale(0.41451706)"
d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z"
stroke-linecap="round"
id="path5"
inkscape:label="hole" /></g>&#10;</svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" id="svg5" width="235" height="500" version="1.1" viewBox="0 0 235 500" xml:space="preserve"><desc id="desc1">Created with Fabric.js 5.3.0</desc><path style="stroke:#6e0b30;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#343434;fill-rule:nonzero;opacity:1" id="path1" stroke-linecap="round" d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z" transform="matrix(0.58757483,0,0,3.9476902,197.13549,250.04675)" vector-effect="non-scaling-stroke"/><path style="stroke:#6e0b30;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:red;fill-rule:nonzero;opacity:1" id="path2" stroke-linecap="round" d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z" transform="matrix(0.38157612,0,0,3.4623174,197.13549,250.04674)" vector-effect="non-scaling-stroke"/><path style="stroke:#000;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:red;fill-rule:nonzero;opacity:1" id="path3" stroke-linecap="round" d="M -38.50267 -65.24064 L 38.502680000000005 -65.24064 L 38.502680000000005 65.24064 L -38.50267 65.24064 z" transform="matrix(2.073642,0,0,3.3577086,117.29471,250.04674)" vector-effect="non-scaling-stroke"/><g id="OvgM38-6CZm9R47a1TDIX" transform="matrix(0.58757483,0,0,3.9476902,37.453957,250.04674)"><filter id="SVGID_1" width="1.272" height="1.114" x="-.057" y="-.057"><feGaussianBlur id="feGaussianBlur3" in="SourceAlpha" stdDeviation="3"/><feOffset id="feOffset3" dx="20" dy="0" result="oBlur"/><feFlood id="feFlood3" flood-color="#000" flood-opacity=".67"/><feComposite id="feComposite3" in2="oBlur" operator="in"/><feMerge id="feMerge4"><feMergeNode id="feMergeNode3"/><feMergeNode id="feMergeNode4" in="SourceGraphic"/></feMerge></filter><path style="stroke:#6e0b30;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#343434;fill-rule:nonzero;opacity:1;filter:url(#SVGID_1)" id="path4" stroke-linecap="round" d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z" transform="translate(8.4959392e-7)" vector-effect="non-scaling-stroke"/><path style="stroke:#6e0b30;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#111;fill-rule:nonzero;opacity:1" id="path5" stroke-linecap="round" d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z" transform="scale(0.41451706)" vector-effect="non-scaling-stroke"/></g></svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -1,61 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
width="220"
height="220"
viewBox="0 0 220 220"
xml:space="preserve"
id="svg8"
sodipodi:docname="spoolman_monochrome.svg"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview8"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="2.6659091"
inkscape:cx="73.333333"
inkscape:cy="97.1526"
inkscape:window-width="1898"
inkscape:window-height="1360"
inkscape:window-x="684"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg8" />&#10;<desc
id="desc1">Created with Fabric.js 5.3.0</desc>&#10;<defs
id="defs1"><inkscape:path-effect
effect="powerclip"
id="path-effect13"
is_visible="true"
lpeversion="1"
inverse="true"
flatten="false"
hide_clip="false"
message="Use fill-rule evenodd on &lt;b&gt;fill and stroke&lt;/b&gt; dialog if no flatten result after convert clip to paths." />&#10;<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath13"><path
id="path13"
style="display:none;opacity:1;fill:#bebebe;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
inkscape:label="bean1_fg"
d="m -13.757715,-7.7471481 0.343537,-0.55719 v 0 c 2.876446,-4.6653799 7.9428216,-7.5315019 13.42331996,-7.5937639 l 0.37756065,-0.0043 0.0795148,8.0507248 v 0 c 0.0172956,1.7514341 -0.91182739,3.3759065 -2.43022861,4.2489907 -1.5184012,0.8730843 -3.3897584,0.8588954 -4.8947484,-0.037114 z M 0.4322926,16.185974 -0.22201757,16.167056 v 0 C -5.7005765,16.00867 -10.715899,13.054123 -13.510068,8.3390037 l -0.192495,-0.3248328 6.9323756,-4.0942248 v 0 c 1.5081376,-0.8906958 3.3795333,-0.8982879 4.894848,-0.019866 1.51531394,0.8784316 2.43870418,2.5061695 2.41523158,4.2575347 z m 13.3375784,-24.319362 0.310774,0.5761074 v 0 c 2.602114,4.8237643 2.551061,10.6444349 -0.135267,15.4218159 L 13.76031,8.1936583 6.7484202,4.2371568 v 0 C 5.2229859,3.3764201 4.2807128,1.7595388 4.2838,0.00802386 4.2868948,-1.7434936 5.2348557,-3.3570423 6.7633174,-4.2123972 Z" /><path
id="lpe_path-effect13"
style="opacity:1;fill:#bebebe;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
class="powerclip"
d="m -27.46181,-27.46181 h 54.92362 v 54.92362 h -54.92362 z m 13.704095,19.7146619 6.8989554,4.1073476 c 1.50499,0.8960094 3.3763472,0.9101983 4.8947484,0.037114 1.51840122,-0.8730842 2.44752421,-2.4975566 2.43022861,-4.2489907 v 0 l -0.0795148,-8.0507248 -0.37756065,0.0043 c -5.48049836,0.06226 -10.54687396,2.928384 -13.42331996,7.5937639 v 0 z M 0.4322926,16.185974 0.53989218,8.1576148 C 0.56336478,6.4062496 -0.36002546,4.7785117 -1.8753394,3.9000801 -3.3906541,3.0216582 -5.2620498,3.0292503 -6.7701874,3.9199461 v 0 l -6.9323756,4.0942248 0.192495,0.3248328 c 2.794169,4.7151193 7.8094915,7.6696663 13.28805043,7.8280523 v 0 z M 13.769871,-8.133388 6.7633174,-4.2123972 C 5.2348557,-3.3570423 4.2868948,-1.7434936 4.2838,0.00802386 4.2807128,1.7595388 5.2229859,3.3764201 6.7484202,4.2371568 v 0 l 7.0118898,3.9565015 0.185068,-0.329123 c 2.686328,-4.777381 2.737381,-10.5980516 0.135267,-15.4218159 v 0 z" /></clipPath></defs>&#10;&#10;&#10;&#10;&#10;&#10;&#10;&#10;<path
style="stroke:rgb(89, 142, 199);stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#000000;fill-rule:nonzero;opacity:1;fill-opacity:1"
vector-effect="non-scaling-stroke"
transform="matrix(4.0068,0,0,4.0068,110,110)"
d="M 0,-22.46181 C 12.39892,-22.46181 22.46181,-12.39892 22.46181,0 22.46181,12.39892 12.39892,22.46181 0,22.46181 -12.39892,22.46181 -22.46181,12.39892 -22.46181,0 -22.46181,-12.39892 -12.39892,-22.46181 0,-22.46181 Z"
stroke-linecap="round"
id="path2"
inkscape:label="spoolface"
clip-path="url(#clipPath13)"
inkscape:path-effect="#path-effect13"
inkscape:original-d="M 0 -22.46181 C 12.39892 -22.46181 22.46181 -12.39892 22.46181 0 C 22.46181 12.39892 12.39892 22.46181 0 22.46181 C -12.39892 22.46181 -22.46181 12.39892 -22.46181 0 C -22.46181 -12.39892 -12.39892 -22.46181 0 -22.46181 z" />&#10;</svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" id="svg8" width="220" height="220" version="1.1" viewBox="0 0 220 220" xml:space="preserve"><desc id="desc1">Created with Fabric.js 5.3.0</desc><defs id="defs1"><clipPath id="clipPath13" clipPathUnits="userSpaceOnUse"><path id="path13" style="display:none;opacity:1;fill:#bebebe;fill-rule:nonzero;stroke:#fff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" d="m -13.757715,-7.7471481 0.343537,-0.55719 v 0 c 2.876446,-4.6653799 7.9428216,-7.5315019 13.42331996,-7.5937639 l 0.37756065,-0.0043 0.0795148,8.0507248 v 0 c 0.0172956,1.7514341 -0.91182739,3.3759065 -2.43022861,4.2489907 -1.5184012,0.8730843 -3.3897584,0.8588954 -4.8947484,-0.037114 z M 0.4322926,16.185974 -0.22201757,16.167056 v 0 C -5.7005765,16.00867 -10.715899,13.054123 -13.510068,8.3390037 l -0.192495,-0.3248328 6.9323756,-4.0942248 v 0 c 1.5081376,-0.8906958 3.3795333,-0.8982879 4.894848,-0.019866 1.51531394,0.8784316 2.43870418,2.5061695 2.41523158,4.2575347 z m 13.3375784,-24.319362 0.310774,0.5761074 v 0 c 2.602114,4.8237643 2.551061,10.6444349 -0.135267,15.4218159 L 13.76031,8.1936583 6.7484202,4.2371568 v 0 C 5.2229859,3.3764201 4.2807128,1.7595388 4.2838,0.00802386 4.2868948,-1.7434936 5.2348557,-3.3570423 6.7633174,-4.2123972 Z"/><path id="lpe_path-effect13" style="opacity:1;fill:#bebebe;fill-rule:nonzero;stroke:#fff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" d="m -27.46181,-27.46181 h 54.92362 v 54.92362 h -54.92362 z m 13.704095,19.7146619 6.8989554,4.1073476 c 1.50499,0.8960094 3.3763472,0.9101983 4.8947484,0.037114 1.51840122,-0.8730842 2.44752421,-2.4975566 2.43022861,-4.2489907 v 0 l -0.0795148,-8.0507248 -0.37756065,0.0043 c -5.48049836,0.06226 -10.54687396,2.928384 -13.42331996,7.5937639 v 0 z M 0.4322926,16.185974 0.53989218,8.1576148 C 0.56336478,6.4062496 -0.36002546,4.7785117 -1.8753394,3.9000801 -3.3906541,3.0216582 -5.2620498,3.0292503 -6.7701874,3.9199461 v 0 l -6.9323756,4.0942248 0.192495,0.3248328 c 2.794169,4.7151193 7.8094915,7.6696663 13.28805043,7.8280523 v 0 z M 13.769871,-8.133388 6.7633174,-4.2123972 C 5.2348557,-3.3570423 4.2868948,-1.7434936 4.2838,0.00802386 4.2807128,1.7595388 5.2229859,3.3764201 6.7484202,4.2371568 v 0 l 7.0118898,3.9565015 0.185068,-0.329123 c 2.686328,-4.777381 2.737381,-10.5980516 0.135267,-15.4218159 v 0 z" class="powerclip"/></clipPath></defs><path style="stroke:#598ec7;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#000;fill-rule:nonzero;opacity:1;fill-opacity:1" id="path2" stroke-linecap="round" d="M 0,-22.46181 C 12.39892,-22.46181 22.46181,-12.39892 22.46181,0 22.46181,12.39892 12.39892,22.46181 0,22.46181 -12.39892,22.46181 -22.46181,12.39892 -22.46181,0 -22.46181,-12.39892 -12.39892,-22.46181 0,-22.46181 Z" clip-path="url(#clipPath13)" transform="matrix(4.0068,0,0,4.0068,110,110)" vector-effect="non-scaling-stroke"/></svg>

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -2,12 +2,12 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#DC7734" />
<meta name="description" content="Spoolman - Keep track of your inventory of 3D-printer filament spools." />
<meta name="robots" content="noindex,nofollow" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<script type="text/javascript" src="./config.js"></script>
<title>Spoolman</title>
</head>

4327
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +1,55 @@
{
"name": "spoolman-ui",
"version": "0.16.1",
"version": "0.19.3",
"engines": {
"node": "20.x"
},
"private": true,
"type": "module",
"dependencies": {
"axios": "^1.6.2",
"@ant-design/icons": "^5.2.6",
"@loadable/component": "^5.16.2",
"@refinedev/antd": "^5.37.0",
"@refinedev/core": "^4.46.0",
"@refinedev/kbar": "^1.3.4",
"@refinedev/react-router-v6": "^4.5.4",
"@refinedev/simple-rest": "^5.0.0",
"@ant-design/icons": "^5.4.0",
"@loadable/component": "^5.16.4",
"@refinedev/antd": "^5.42.0",
"@refinedev/core": "^4.53.0",
"@refinedev/kbar": "^1.3.12",
"@refinedev/react-router-v6": "^4.5.11",
"@refinedev/simple-rest": "^5.0.8",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-query-devtools": "^4.36.1",
"@types/loadable__component": "^5.13.8",
"@yudiel/react-qr-scanner": "^1.2.4",
"antd": "^5.12.2",
"flag-icons": "^7.1.0",
"i18next": "^23.7.11",
"i18next-browser-languagedetector": "^7.2.0",
"i18next-xhr-backend": "^3.2.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^13.5.0",
"react-router-dom": "^6.21.0",
"react-to-print": "^2.14.15",
"@types/loadable__component": "^5.13.9",
"@types/lodash": "^4.17.7",
"@types/uuid": "^10.0.0",
"@yudiel/react-qr-scanner": "^1.2.10",
"antd": "^5.20.0",
"axios": "^1.7.3",
"flag-icons": "^7.2.3",
"html-to-image": "^1.11.11",
"i18next": "^23.12.2",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.5.2",
"lodash": "^4.17.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.0.0",
"react-router-dom": "^6.26.0",
"react-to-print": "^2.15.1",
"uuid": "^10.0.0",
"vite-plugin-svgr": "^4.2.0"
},
"devDependencies": {
"@refinedev/cli": "^2.16.20",
"@types/node": "^20.10.4",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite-plugin-mkcert": "^1.17.1"
"@refinedev/cli": "^2.16.36",
"@types/node": "^20.14.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^9.8.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.9",
"typescript": "^5.5.4",
"vite": "^5.3.5",
"vite-plugin-mkcert": "^1.17.5"
},
"scripts": {
"dev": "refine dev",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -28,7 +28,8 @@
"notAccessTitle": "Nemáte oprávnění k přístupu",
"hideColumns": "Skrýt sloupce",
"clearFilters": "Zrušit filtry",
"saveAndAdd": "Uložit a přidat"
"saveAndAdd": "Uložit a přidat",
"continue": "Pokračovat"
},
"warnWhenUnsavedChanges": "Jste si jisti, že chcete odejít? Máte neuložené změny.",
"notifications": {
@@ -87,16 +88,27 @@
"none": "Žádný",
"border": "Ohraničení",
"grid": "Mřížka"
}
},
"itemCopies": "Kopie položky",
"defaultSettings": "Výchozí",
"addSettings": "Přidat novou předvolbu",
"newSetting": "Nový",
"deleteSettings": "Smazat aktuální předvolbu",
"settingsName": "Název předvolby",
"settings": "Předvolby",
"deleteSettingsConfirm": "Opravdu chcete tuto předvolbu smazat?",
"duplicateSettings": "Duplikát",
"saveSetting": "Uložit předvolby",
"saveAsImage": "Uložit jako obrázek"
},
"qrcode": {
"button": "Tisk QR kódů",
"title": "Tisk QR kódů",
"button": "Tisk štítků",
"title": "Tisk štítku",
"spoolWeight": "Hmotnost cívky: {{weight}}",
"lotNr": "Číslo šarže: {{lot}}",
"bedTemp": "BT: {{temp}}",
"extruderTemp": "ET: {{temp}}",
"textSize": "Velikost textu obsahu",
"textSize": "Velikost textu štítku",
"showSpoolmanIcon": "Zobrazit ikonu Spoolman",
"showVendor": "Výrobce",
"showContent": "Tisk štítku",
@@ -109,11 +121,19 @@
"useHTTPUrl": {
"label": "Použít HTTP(S) URL",
"tooltip": "Použít HTTP(S) odkaz v QR kódu. Pokud je vypnuto, použije se speciální kód, který půjde načíst pouze skrz skener v aplikaci Spoolman."
},
"template": "Šablona štítku",
"templateHelp": "Pomocí {} vložíte hodnoty objektu cívky jako text. Například {id} bude nahrazeno ID cívky nebo {filament.material} bude nahrazeno materiálem cívky. Text uzavřete dvojitou hvězdičkou **, aby byl tučný. Kliknutím na tlačítko zobrazíte seznam všech dostupných značek.",
"showQRCode": "Tisk QR kódu",
"showQRCodeMode": {
"no": "Ne",
"simple": "Jednoduchý",
"withIcon": "S ikonou"
}
},
"spoolSelect": {
"title": "Vybrat cívky",
"description": "Vyberte cívky pro tisk QR kódů.",
"description": "Vyberte cívky, pro které chcete tisknout štítky.",
"showArchived": "Zobrazit archivované",
"noSpoolsSelected": "Nevybrali jste žádné cívky.",
"selectAll": "Vybrat/odebrat vše",
@@ -148,13 +168,17 @@
"used_length": "Použitá délka",
"remaining_length": "Zbývající délka",
"location": "Umístění",
"lot_nr": "Číslo umístění",
"lot_nr": "Číslo šarže",
"first_used": "První použití",
"last_used": "Naposledy použitá",
"registered": "Registrováno",
"comment": "Komentář",
"archived": "Archivováno",
"price": "Cena"
"price": "Cena",
"spool_weight": "Prázdná hmotnost",
"initial_weight": "Výchozí hmotnost",
"filament_internal": "Interní",
"filament_external": "Externí"
},
"fields_help": {
"weight_to_use": "Vyberte, jakou hodnotu hmotnosti chcete zadat. Naměřená hmotnost je k dispozici pouze v případě, že je pro vybraný filament nastavena hmotnost cívky.",
@@ -163,7 +187,10 @@
"measured_weight": "Jaká je hmotnost filamentu a cívky.",
"location": "Kde se cívka nachází, pokud máte více míst, kde cívky skladujete.",
"lot_nr": "Číslo šarže výrobce. Lze použít k zajištění jednotné barvy tisku v případě použití více cívek.",
"price": "Cena celé cívky v měně nastavené v systému. Pokud není nastavena, předpokládá se cena filamentu."
"price": "Cena celé cívky. Pokud není nastavena, předpokládá se cena filamentu.",
"spool_weight": "Hmotnost prázdné cívky. Nechte prázdné, chcete-li použít hodnotu z filamentu nebo od výrobce.",
"initial_weight": "Počáteční hmotnost vlákna na cívce (čistá hmotnost). Pokud není nastavena, použije se hmotnost z objektu filament.",
"external_filament": "Vybrali jste filament z externí databáze. Objekt filamentu (a případně i objekt výrobce) se vytvoří automaticky při vytvoření této cívky. Tím se mohou vytvořit duplicitní objekty filamentů, pokud jste již objekt filamentu pro toto vlákno vytvořili."
},
"titles": {
"create": "Vytvořit cívku",
@@ -172,14 +199,22 @@
"list": "Cívky",
"show": "Zobrazit cívku",
"archive": "Archivní cívka",
"show_title": "[Cívka #{{id}}] {{name}}"
"show_title": "[Cívka #{{id}}] {{name}}",
"adjust": "Nastavení cívky filamentu"
},
"messages": {
"archive": "Opravdu chcete tuto cívku archivovat?"
},
"form": {
"new_location_prompt": "Zadejte nové umístění",
"spool_updated": "Od otevření této stránky byla tato cívka někým/něčím aktualizována. Uložením se tyto změny přepíší!"
"spool_updated": "Od otevření této stránky byla tato cívka někým/něčím aktualizována. Uložením se tyto změny přepíší!",
"measurement_type": {
"length": "Délka",
"weight": "Hmotnost"
},
"measurement_type_label": "Typ měření",
"adjust_filament_help": "Zde můžete přímo přidávat nebo odebírat filament z cívky. Kladná hodnota filament odebere, záporná hodnota ho přidá.",
"adjust_filament_value": "Spotřebované množství"
}
},
"filament": {
@@ -201,15 +236,21 @@
"settings_extruder_temp": "Teplota extruderu",
"settings_bed_temp": "Teplota podložky",
"color_hex": "Barva",
"spools": "Zobrazit cívky"
"spools": "Zobrazit cívky",
"external_id": "Externí ID",
"single_color": "Jediný",
"coaxial": "Koextrudovaný",
"longitudinal": "Podélná",
"multi_color": "Více"
},
"fields_help": {
"name": "Název filamentu, aby se tento typ filamentu odlišil od ostatních filamentů od stejného dodavatele. Měl by obsahovat například barvu.",
"name": "Název filamentu, aby se tento typ vlákna odlišil od ostatních filamentů od stejného výrobce. Měl by obsahovat například barvu.",
"material": "Např. PLA, ABS, ASA, PETG, atd.",
"price": "Cena celé cívky v měně nastavené v systému.",
"price": "Cena celé cívky.",
"weight": "Hmotnost plného cívky filamentu (čistá hmotnost). Tato hmotnost by neměla zahrnovat hmotnost samotné cívky, ale pouze filamentu. Obvykle se uvádí na obalu.",
"spool_weight": "Hmotnost prázdné cívky. Slouží k určení naměřené hmotnosti cívky.",
"article_number": "Např. EAN, UPC, atd."
"article_number": "Např. EAN, UPC, atd.",
"multi_color_direction": "Filamenty mohou mít více barev dvěma způsoby: buď koextruzí jako dvoubarevná vlákna s konzistentními více barvami, nebo podélnými změnami barev jako gradientní vlákna, která mění barvy podél cívky."
},
"titles": {
"create": "Vytvořit filament",
@@ -220,7 +261,9 @@
"show_title": "[Filament #{{id}}] {{name}}"
},
"form": {
"filament_updated": "Toto vlákno bylo od doby, kdy jste otevřeli tuto stránku, někým/něčím aktualizováno. Uložením se tyto změny přepíší!"
"filament_updated": "Toto vlákno bylo od doby, kdy jste otevřeli tuto stránku, někým/něčím aktualizováno. Uložením se tyto změny přepíší!",
"import_external": "Import z externího zdroje",
"import_external_description": "Výběrem filamentu v seznamu se automaticky vyplní jeho údaje ve formuláři pro vytvoření filamentu. Tím se přepíší všechny údaje, které jste do formuláře zadali.<br><br>Po kliknutí na tlačítko OK se v případě potřeby automaticky vytvoří objekt Výrobce."
},
"buttons": {
"add_spool": "Přidat cívku"
@@ -232,7 +275,9 @@
"id": "Id",
"name": "Název",
"registered": "Registrováno",
"comment": "Komentář"
"comment": "Komentář",
"empty_spool_weight": "Hmotnost prázdné cívky",
"external_id": "Externí ID"
},
"titles": {
"create": "Vytvořit výrobce",
@@ -244,6 +289,9 @@
},
"form": {
"vendor_updated": "Od otevření této stránky byl tento prodejce někým/něčím aktualizován. Uložením se tyto změny přepíší!"
},
"fields_help": {
"empty_spool_weight": "Hmotnost prázdné cívky od tohoto výrobce."
}
},
"table": {
@@ -286,7 +334,7 @@
"filament": "Značky vláken. Mají vlastnosti, jako je název, materiál, barva, průměr a další.",
"spool": "Jednotlivé fyzické cívky určitého vlákna."
},
"description": "<title>Nápověda</title><p>Zde je několik tipů, které vám pomohou začít.</p><p>Spoolman uchovává 3 různé typy dat:</p><itemsHelp/><p>Chcete-li do databáze přidat novou cívku, začněte vytvořením položky <filamentCreateLink>Filament</filamentCreateLink> objekt. Jakmile to uděláte, vytvořte položku <spoolCreateLink>Cívka</spoolCreateLink> popis pro danou cívku. Pokud později získáte další cívky stejného filamentu, jednoduše vygenerujte další objekty Cívky a znovu použijte stejný objekt Filament.</p><p>Volitelně můžete také vygenerovat <vendorCreateLink>Výrobce</vendorCreateLink> název společnosti vyrábějící vlákno, pokud si přejete tyto informace sledovat.</p><p>Máte možnost propojit se Spoolmanem další služby 3D tiskárny, například Moonraker, který může automaticky sledovat využití filamentu a aktualizovat objekty Cívek za vás. Pokyny k nastavení najdete v <readmeLink>Spoolman README</readmeLink>.</p>",
"description": "<title>Nápověda</title><p>Zde je několik tipů, které vám pomohou začít.</p><p>Spoolman ukládá 3 různé typy dat:</p><itemsHelp/><p>Chcete-li do databáze přidat novou cívku, začněte vytvořením položky <filamentCreateLink>Filament</filamentCreateLink> . Jakmile to uděláte, vytvořte položku <spoolCreateLink>Cívka</spoolCreateLink> s popisem pro danou cívku. Pokud později získáte další cívky stejného filamentu, jednoduše vygenerujte další Cívku a použijte opět stejný Filament.</p><p>Volitelně můžete také vygenerovat <vendorCreateLink>Výrobce</vendorCreateLink> název společnosti vyrábějící filamenty, pokud si přejete tyto informace sledovat.</p><p>Máte možnost propojit se Spoolmanem další služby 3D tiskárny, například Moonraker, který může automaticky sledovat využití filamentu a aktualizovat dané Cívky za vás. Pokyny k nastavení najdete v <readmeLink>Spoolman README</readmeLink>.</p>",
"help": "Nápověda"
},
"kofi": "Příspěvek autorovi na Ko-fi",
@@ -335,6 +383,7 @@
"label": "URL pro QR kódy",
"tooltip": "Základ URL pro generování QR kódů. Pokud můžete přistupovat ke svému Spoolmanovi přes více URL, nastavte zde vaší 'kanonickou' URL instance Spoolmana."
}
}
},
"settings": "Nastavení"
}
}

View File

@@ -16,7 +16,9 @@
"bedTemp": "BT: {{temp}}",
"lotNr": "Gruppe nr: {{lot}}",
"showFilamentComment": "Filament kommentarer",
"button": "Print QR koder"
"button": "Print QR koder",
"template": "Skabelon",
"templateHelp": "Brug {} til at indsætte værdier for spolen som tekst. For eksempen vil {id} blive erstattet med spolens id, eller {filament.material} vil blive erstattet med materialet på spolen. Omgiv teksten med dobbelte asterisk ** for at gøre teksten fed. Klik på knappen for at vise en liste med mulige tags."
},
"generic": {
"printerMarginBottom": "Safe-Zone bund",
@@ -48,7 +50,15 @@
"showBorder": "Vis ramme",
"printerMarginLeft": "Safe-Zone venstre",
"dimensions": "Dimensioner",
"print": "Udskriv"
"print": "Udskriv",
"settings": "Indstillinger",
"defaultSettings": "Standard",
"addSettings": "Tilføj ny indstilling",
"newSetting": "Ny",
"deleteSettings": "Slet nuværende indstilling",
"deleteSettingsConfirm": "Er du sikker på du vil slette denne indstilling?",
"settingsName": "Indstillingsnavn",
"itemCopies": "Kopier"
},
"spoolSelect": {
"selectedTotal_one": "{{count}} spole valgt",
@@ -62,11 +72,11 @@
},
"documentTitle": {
"vendor": {
"create": "Opret sælger | Spoolman",
"clone": "#{{id}} Klon sælger | Spoolman",
"edit": "#{{id}} Redigér sælger | Spoolman",
"list": "Sælgere | Spoolman",
"show": "#{{id}} Vis sælger | Spoolman"
"create": "Opret producent | Spoolman",
"clone": "#{{id}} Klon producent | Spoolman",
"edit": "#{{id}} Redigér producent | Spoolman",
"list": "Producenter | Spoolman",
"show": "#{{id}} Vis producent | Spoolman"
},
"default": "Spoolman",
"spool": {
@@ -110,7 +120,11 @@
"used_weight": "Brugt vægt",
"id": "ID",
"remaining_weight": "Resterende vægt",
"price": "Pris"
"price": "Pris",
"initial_weight": "Startvægt",
"spool_weight": "Tom vægt",
"filament_internal": "Intern",
"filament_external": "Ekstern"
},
"titles": {
"archive": "Arkivér spole",
@@ -132,7 +146,10 @@
"lot_nr": "Producentens \"batchnummer\". Kan bruges til at sikre ensartede farver ved brug af flere ruller.",
"remaining_weight": "Hvor meget filament er der tilbage på spolen. For en ny spole bør dette matches spolens vægt.",
"location": "Placering af spolen. Hvis du har flere lokationer.",
"price": "Pris for en fuld spole, i den i systemet konfigurerede valuta. Hvis ikke indstillet, vil prisen bliver estimeret."
"price": "Pris for en fuld spole. Hvis ikke indstillet, vil prisen bliver estimeret.",
"external_filament": "Du har valgt et Filament fra den eksterne database, Et Filament (og muligvis en Producent) vil blive oprettet automatisk når du gemmer denne spole. Dette kan oprette duplikerede objekter, hvis du allerede har dette filament oprettet.",
"initial_weight": "Startvægten af filament på spolen (nettovægt). Bruger filamentets standardvægt hvis ikke angivet.",
"spool_weight": "Vægten af spolen når den er tom. Et tomt felt vil bruge værdien fra Filament eller Producent."
},
"spool": "Spoler",
"messages": {
@@ -168,13 +185,14 @@
"name": "Filament navn, for at adskille dette filament fra andre fra samme producent. Kan for eksempel indeholde farven.",
"article_number": "f.eks. EAN, UPC osv.",
"spool_weight": "Vægten af en tom spole. Bruges til at angive den målte vægt af en spole.",
"price": "Pris for en ny spole i systemets valutaenhed.",
"price": "Pris for en ny spole.",
"weight": "Filamentets vægt på en ny spole (Netto vægt). Dette bør ikke inkludere vægten af selve spolen, kun filamentet. Det er som regel den angivne vægt på pakken.",
"material": "f.eks. PLA, ABS, PETG osv."
"material": "f.eks. PLA, ABS, PETG osv.",
"multi_color_direction": "Filamenter kan have flere farver på to måder: Enten koaksialt, som 2- & 3-farvede filamenter med vedvarende flerfarvethed, eller gennem længdeværts ændringer, som regnbuefilamenter, hvor farven skifter undervejs på spolen."
},
"fields": {
"material": "Materiale",
"vendor_name": "Sælger",
"vendor_name": "Producent",
"settings_extruder_temp": "Extruder temperatur",
"comment": "Kommentar",
"diameter": "Diameter",
@@ -184,12 +202,17 @@
"name": "Navn",
"price": "Pris",
"settings_bed_temp": "Bed temperatur",
"vendor": "Sælger",
"vendor": "Producent",
"density": "Massefylde",
"id": "ID",
"color_hex": "Farve",
"article_number": "Produkt nummer",
"spools": "Vis spoler"
"spools": "Vis spoler",
"external_id": "Eksternt ID",
"longitudinal": "Længdeværts",
"multi_color": "Flerfarvet",
"coaxial": "Koaksialt",
"single_color": "Enkeltfarvet"
},
"titles": {
"clone": "Klon filament",
@@ -200,9 +223,14 @@
"list": "Filamenter"
},
"form": {
"filament_updated": "Dette filament er blevet redigeret af nogen/noget andet siden du åbnede denne side. Hvis du gemmer overskrives disse ændringer!"
"filament_updated": "Dette filament er blevet redigeret af nogen/noget andet siden du åbnede denne side. Hvis du gemmer overskrives disse ændringer!",
"import_external": "Importér fra Eksternt",
"import_external_description": "Vælg et filament på listen for automatisk at udfylde formularen. Dette vil overskrive alle data du allerede har indtastet i formularen. <br><br> En Producent vil blive oprettet automatisk hvis nødvendigt, når du klikker OK."
},
"filament": "Filamenter"
"filament": "Filamenter",
"buttons": {
"add_spool": "Tilføj spole"
}
},
"actions": {
"create": "Opret",
@@ -213,23 +241,28 @@
},
"vendor": {
"titles": {
"show_title": "[Sælger #{{id}}] {{name}}",
"show": "Vis sælger",
"edit": "Redigér sælger",
"clone": "Klon sælger",
"create": "Opret sælger",
"list": "Sælgere"
"show_title": "[Producent #{{id}}] {{name}}",
"show": "Vis producent",
"edit": "Redigér producent",
"clone": "Klon producent",
"create": "Opret producent",
"list": "Producenter"
},
"form": {
"vendor_updated": "Denne sælger er blevet redigeret af nogen/noget andet siden du åbnede denne side. Hvis du gemmer overskrives disse ændringer!"
"vendor_updated": "Denne producent er blevet redigeret af nogen/noget andet siden du åbnede denne side. Hvis du gemmer overskrives disse ændringer!"
},
"fields": {
"registered": "Registreret",
"comment": "Kommentar",
"name": "Navn",
"id": "ID"
"id": "ID",
"external_id": "Eksternt ID",
"empty_spool_weight": "Tom Spolevægt"
},
"vendor": "Sælgere"
"vendor": "Producenter",
"fields_help": {
"empty_spool_weight": "Vægten af en tom spole fra denne producent."
}
},
"scanner": {
"title": "QR kode skanner",
@@ -253,7 +286,9 @@
"success": "Succes",
"editSuccess": "Redigering af {{resource}} lykkedes",
"deleteError": "Fejl ved sletning af {{resource}} (status kode: {{statusCode}})",
"createSuccess": "Oprettelse af {{resource}} lykkedes"
"createSuccess": "Oprettelse af {{resource}} lykkedes",
"saveSuccessful": "Gemt!",
"validationError": "Kunne ikke gennemføres: {{error}}"
},
"no": "Nej",
"unknown": "Ukendt",
@@ -276,11 +311,52 @@
"filament": "Filament mærker. De har værdier såsom Navn, Materiale, Farve, Diameter m.fl.",
"spool": "Individuelle fysiske spoler af et specifikt filament."
},
"description": "<title>Hjælp</title><p>Her er nogle tips til at komme i gang.</p><p>Spoolman indeholder 3 forskellige typer data:</p><itemsHelp/><p>For at tilføje en ny spole til databasen, skal du starte med at oprette et <filamentCreateLink>Filament</filamentCreateLink> objekt. Når dette er gjort, fortsættes der ved at oprette et <spoolCreateLink>Spole</spoolCreateLink> objekt for den specifikke spole. Hvis du har flere spoler af samme filament, opretter du blot et nyt spole objekt og genbruger samme filament. </p><p> Du har også mulighed for at oprette et <vendorCreateLink>Forhandler</vendorCreateLink> objekt for firmaet der producerer filamentet, hvis du ønsker.</p><p>Du har mulighed for at linke andre 3D printer services til Spoolman, Som f.eks. Moonraker. Dette giver mulighed for automatisk at opdatere og overvåge forbruget af filament for hvert print. Læs <readmeLink>Spoolman README</readmeLink> for information om opsætning af dette.</p>",
"description": "<title>Hjælp</title><p>Her er nogle tips til at komme i gang.</p><p>Spoolman indeholder 3 forskellige typer data:</p><itemsHelp/><p>For at tilføje en ny spole til databasen, skal du starte med at oprette et <filamentCreateLink>Filament</filamentCreateLink> objekt. Når dette er gjort, fortsættes der ved at oprette et <spoolCreateLink>Spole</spoolCreateLink> objekt for den specifikke spole. Hvis du har flere spoler af samme filament, opretter du blot et nyt spole objekt og genbruger samme filament. </p><p> Du har også mulighed for at oprette et <vendorCreateLink>Producent</vendorCreateLink> objekt for firmaet der producerer filamentet, hvis du ønsker.</p><p>Du har mulighed for at linke andre 3D printer services til Spoolman, Som f.eks. Moonraker. Dette giver mulighed for automatisk at opdatere og overvåge forbruget af filament for hvert print. Læs <readmeLink>Spoolman README</readmeLink> for information om opsætning af dette.</p>",
"help": "Hjælp"
},
"kofi": "Donér på Ko-fi",
"home": {
"home": "Hjem"
},
"settings": {
"header": "Indstillinger",
"general": {
"tab": "Generelt",
"currency": {
"label": "Valuta"
}
},
"extra_fields": {
"params": {
"key": "Nøgle",
"name": "Navn",
"field_type": "Type",
"unit": "Enhed",
"order": "Sortering",
"default_value": "Standard-værdi",
"multi_choice": "Multi-valg",
"choices": "Valgmuligheder"
},
"field_type": {
"integer_range": "Heltals spænd",
"float": "Decimaltal",
"float_range": "Decimaltal spænd",
"datetime": "Dato-tid",
"boolean": "Boolean (Ja/Nej)",
"choice": "Valg",
"text": "Tekst",
"integer": "Heltal"
},
"choices_missing_error": "Du kan ikke fjerne valg. De følgende valg mangler: {{choices}}",
"tab": "Ekstra felter",
"description": "<p>Her kan du tilføje ekstra felter til dine poster.</p><p>Når et felt er tilføjet kan du ikke redigere Nøgle og Type, og for visse felttyper kan du ikke fjerne valg, eller ændre multi-valgs status. Hvis du fjerner et felt, vil associerede data i alle poster blive slettet.</p><p>Nøglen er hvad andre programmer læser/skriver data som, så hvis dit felt skal integreres med en tredje parts program, skal du sørge for at indstille det korrekt. Standard-værdi bliver kun tilføjet til nye felter.</p><p>Ekstra felter kan ikke bruges til sortering eller filtrering i oversigterne.</p>",
"boolean_true": "Ja",
"boolean_false": "Nej",
"non_unique_key_error": "Nøglen skal være Unik.",
"key_not_changed": "Ændr venligst nøglen til noget andet.",
"delete_confirm": "Slet felt: {{name}}?",
"delete_confirm_description": "Dette vil slette feltet samt alle associerede data for alle poster."
},
"settings": "Indstillinger"
}
}

View File

@@ -13,38 +13,40 @@
"delete": "Löschen",
"edit": "Bearbeiten",
"cancel": "Abbrechen",
"confirm": "Sicher?",
"confirm": "bist du sicher?",
"filter": "Filter",
"clear": "Löschen",
"clear": "Bereinigen",
"refresh": "Aktualisieren",
"show": "Anzeigen",
"undo": "Rückgängig",
"import": "Importieren",
"clone": "Duplizieren",
"notAccessTitle": "Keine Berechtigung, um hierauf zuzugreifen",
"hideColumns": "Spalten anpassen",
"clearFilters": "Filter leeren",
"notAccessTitle": "Du hast keine Zugriffsberechtigung",
"hideColumns": "Spalten ausblenden",
"clearFilters": "Filter bereinigen",
"showArchived": "Archivierte anzeigen",
"archive": "Archivieren",
"archive": "Archiv",
"hideArchived": "Archivierte verstecken",
"unArchive": "Archivieren rückgängig",
"saveAndAdd": "Speichern und hinzufügen"
"unArchive": "Archivieren rückgängig machen",
"saveAndAdd": "Speichern und hinzufügen",
"continue": "Weiter"
},
"warnWhenUnsavedChanges": "Sicher? Nicht gespeicherte Änderungen gehen verloren.",
"warnWhenUnsavedChanges": "Bist du sicher, dass du die Seite verlassen willst? Du hast ungespeicherte Änderungen.",
"notifications": {
"success": "Erfolg",
"success": "Erfolgreich",
"error": "Fehler (status code: {{statusCode}})",
"undoable": "Noch {{seconds}} Sekunden um die Aktion rückgängig zu machen.",
"createSuccess": "{{resource}} erfolgreich erstellt",
"createError": "Fehler beim Erstellen {{resource}} (status code: {{statusCode}})",
"createError": "Fehler beim erstellen von {{resource}} (status code: {{statusCode}})",
"deleteSuccess": "{{resource}} erfolgreich gelöscht",
"deleteError": "Fehler beim Löschen {{resource}} (status code: {{statusCode}})",
"deleteError": "Fehler beim löschen von {{resource}} (status code: {{statusCode}})",
"editSuccess": "{{resource}} erfolgreich bearbeitet",
"editError": "Fehler beim Bearbeiten {{resource}} (status code: {{statusCode}})",
"editError": "Fehler beim bearbeiten von {{resource}} (status code: {{statusCode}})",
"importProgress": "{{processed}}/{{total}} importiert",
"saveSuccessful": "Erfolgreich gesichert!"
"saveSuccessful": "Erfolgreich gespeichert!",
"validationError": "Validierungsfehler: {{error}}"
},
"loading": "Laden",
"loading": "Läd",
"version": "Version",
"unknown": "Unbekannt",
"yes": "Ja",
@@ -57,19 +59,19 @@
},
"printing": {
"generic": {
"title": "Drucken",
"description": "Einstellungen unten anpassen, um das gewünschte Drucklayout zu erstellen. Der Drucker oder das Betriebssystem fügen möglichweise eigene Abstände und Skalierungen hinzu. Eventuell muss ein wenig experimentiert werden, bevor alles korrekt funktioniert. Auf normalem Papier testen, bevor auf Label-Papier gedruckt wird.",
"helpMargin": "Abstände sollten so konfiguriert werden, dass sie zum Papier und Drucker passen. Änderungen beeinflussen die Größe des gesamten Gitters.",
"helpPrinterMargin": "Der Sicherheitsabstand sollte so eingestellt werden, wie dicht der Drucker am Rand des Papiers drucken kann. Änderungen beeinflussen nicht die Größe des Gitters.",
"title": "druckt",
"description": "Passe die Einstellungen unten an, um das gewünschte Drucklayout zu bekommen. Denk daran, dass Drucker und dein OS eigene Abstände und Skalierungen anwenden könnte, also musst du vielleicht ein bisschen ausprobieren, bis alles passt. Teste es auf einem normalen Blatt Papier, bevor du auf die richtigen Labels druckst.",
"helpMargin": "Abstände sollten so konfiguriert werden, dass sie zum Papier und Drucker passen. Änderungen beeinflussen die Abmessungen des gesamten Gitters.",
"helpPrinterMargin": "Der Sicherheitsabstand sollte so eingestellt werden, wie dicht der Drucker am Rand des Papiers drucken kann. Änderungen beeinflussen nicht die Abmessungen des Gitters.",
"print": "Drucken",
"columns": "Spalten",
"rows": "Reihen",
"rows": "Zeilen",
"paperSize": "Papiergröße",
"customSize": "Benutzerdefiniert",
"dimensions": "Dimensionen",
"showBorder": "Rahmen anzeigen",
"showBorder": "Ränder anzeigen",
"previewScale": "Vorschau-Skalierung",
"skipItems": "Elemente überspringen",
"skipItems": "Objekt überspringen",
"contentSettings": "Einstellungen: Inhalt",
"layoutSettings": "Einstellungen: Layout",
"horizontalSpacing": "Horizontaler Abstand",
@@ -84,17 +86,28 @@
"printerMarginBottom": "Sicherheitsabstand Unten",
"borders": {
"none": "Keine",
"border": "Rahmen",
"border": "Ränder",
"grid": "Gitter"
}
},
"saveAsImage": "Als Bild speichern",
"settings": "Vorlage",
"defaultSettings": "Standard",
"addSettings": "Neue Vorlage hinzufügen",
"newSetting": "Neu",
"deleteSettings": "Aktuelle Vorlage löschen",
"deleteSettingsConfirm": "Bist du dir sicher, dass du diese Vorlage löschen willst?",
"settingsName": "Vorlagenname",
"itemCopies": "Anzahl der Kopien",
"duplicateSettings": "Kopie erstellen",
"saveSetting": "Vorlage speichern"
},
"qrcode": {
"button": "QR-Codes drucken",
"title": "QR Codes Drucken",
"button": "Beschriftung drucken",
"title": "Beschriftung drucken",
"spoolWeight": "Gewicht Spule: {{weight}}",
"lotNr": "Ch. Nr: {{lot}}",
"lotNr": "Lot. Nr: {{lot}}",
"bedTemp": "BT: {{temp}}",
"extruderTemp": "DT: {{temp}}",
"extruderTemp": "ET: {{temp}}",
"textSize": "Textgröße Label",
"showSpoolmanIcon": "Spoolman Icon anzeigen",
"showVendor": "Hersteller",
@@ -104,13 +117,21 @@
"showTemperatures": "Temperaturen",
"showSpoolComment": "Kommentar: Spule",
"showFilamentComment": "Kommentar: Filament",
"showVendorComment": "Kommentar: Hersteller"
"showVendorComment": "Kommentar: Hersteller",
"showQRCodeMode": {
"no": "Nein",
"simple": "Einfach",
"withIcon": "Mit Icon"
},
"template": "Beschriftung Vorlagen",
"templateHelp": "Benutze {} um Texte der Spule dynamisch zu übernehmen. Zum Beispiel wird {id} durch die ID der Spule ersetzt, {filament.material} wird durch das Material der Spule ersetzt. Texte zwischen zwei Sternen ** werden fett dargestellt. Drücke auf den Knopf um alle verfügbaren Variablen zu sehen.",
"showQRCode": "Drucke QR Code"
},
"spoolSelect": {
"title": "Spulenauswahl",
"description": "Spulen auswählen, für die ein QR-Code gedruckt werden soll.",
"showArchived": "Archivierte anzeigen",
"noSpoolsSelected": "Keine Spulen ausgewählt.",
"noSpoolsSelected": "Du hast keine Spulen ausgewählt.",
"selectAll": "Alle aus-/abwählen",
"selectedTotal_one": "{{count}} Spule ausgewählt",
"selectedTotal_other": "{{count}} Spulen ausgewählt"
@@ -118,7 +139,7 @@
},
"scanner": {
"title": "QR-Code Scanner",
"description": "Spoolman QR-Code scannen, um Details zur Spule anzuzeigen.",
"description": "Spoolman QR-Code scannen, um Details der Spule anzuzeigen.",
"error": {
"notAllowed": "Zugriff auf Kamera wurde nicht erlaubt.",
"insecureContext": "Die Seite wird nicht über HTTPS bereitgestellt.",
@@ -148,16 +169,23 @@
"registered": "Registriert",
"comment": "Kommentar",
"archived": "Archiviert",
"price": "Preis"
"price": "Preis",
"initial_weight": "Anfangsgewicht",
"spool_weight": "Leergewicht",
"filament_internal": "Intern",
"filament_external": "Extern"
},
"fields_help": {
"weight_to_use": "Auswählen welches Gewicht verwendet werden soll. \"Gemessenes Gewicht\" ist nur verfügbar, wenn das Gewicht der Spule für das Filament hinterlegt ist.",
"weight_to_use": "Auswählen, welches Gewicht verwendet werden soll. Das gemessenes Gewicht ist nur verfügbar, wenn das Gewicht der Spule für das Filament hinterlegt ist.",
"used_weight": "Wie viel Gewicht bereits verbraucht wurde z.B. 0g für eine neue Spule.",
"remaining_weight": "Wie viel Filament noch auf der Spule ist. Entspricht dem auf der Spule angegeben Nettogewicht, wenn die Spule neu ist.",
"measured_weight": "Gesamtgewicht von Spule und Filament.",
"location": "Ort, an dem die Spule gelagert wird.",
"lot_nr": "Chargennummer des Herstellers. Kann verwendet werden um gleichmäßige Farben zu garantieren, wenn mehrere Spulen verwendet werden.",
"price": "Preis einer vollen Spule in der im System konfigurierten Währung. Falls nicht ausgewählt, wird der Preis des Filaments geschätzt."
"price": "Preis einer vollen Spule. Falls nicht ausgewählt, wird der Preis des Filaments geschätzt.",
"initial_weight": "Das Anfangsgewicht des Filaments (Nettogewicht) auf der Spule. Wenn nicht gesetzt, dann wird der Wert aus dem Filament Objekt genutzt.",
"spool_weight": "Das Gewicht der leeren Spule. Leer lassen um das Gewicht vom Hersteller zu nutzen.",
"external_filament": "Sie haben ein Filament aus der externen Datenbank ausgewählt. Ein Filamentobjekt (und möglicherweise ein Herstellerobjekt) wird automatisch erstellt, wenn Sie diese Spule erstellen. Dadurch können doppelte Filamentobjekte entstehen, wenn Sie bereits ein Filamentobjekt für dieses Filament erstellt haben."
},
"titles": {
"create": "Spule erstellen",
@@ -166,14 +194,21 @@
"list": "Spulen",
"show": "Spule anzeigen",
"archive": "Spule archivieren",
"show_title": "[Spule #{{id}}] {{name}}"
"show_title": "[Spule #{{id}}] {{name}}",
"adjust": "Filamentspulen Anpassung"
},
"messages": {
"archive": "Sicher das die Spule archiviert werden soll?"
},
"form": {
"spool_updated": "Diese Spule wurde, seit dem die Seite geöffnet wurde, von jemand anderem/etwas anderem bearbeitet. Speichern wird diese Änderungen überschreiben!",
"new_location_prompt": "Neue Position eingeben"
"new_location_prompt": "Neue Position eingeben",
"adjust_filament_help": "Hier kannst du direkt Filament von der Spule hinzufügen oder entfernen. Eine positive Eingabe wird Filament als verbraucht markieren, eine negative wird es der Spule hinzufügen.",
"measurement_type": {
"length": "Länge",
"weight": "Gewicht"
},
"adjust_filament_value": "Verbrauchte Menge"
}
},
"filament": {
@@ -195,7 +230,10 @@
"settings_extruder_temp": "Düsentemperatur",
"settings_bed_temp": "Betttemperatur",
"color_hex": "Farbe",
"spools": "Zeige Spulen"
"spools": "Zeige Spulen",
"external_id": "Externe ID",
"single_color": "Einfarbig",
"multi_color": "Mehrfarbig"
},
"fields_help": {
"name": "Name des Filaments um es von anderen Typen des gleichen Herstellers zu unterscheiden. Sollte z.B. die Farbe enthalten.",
@@ -214,7 +252,12 @@
"show_title": "[Filament #{{id}}] {{name}}"
},
"form": {
"filament_updated": "Dieses Filament wurde, seit dem die Seite geöffnet wurde, von jemand anderem/etwas anderem bearbeitet. Speichern wird diese Änderungen überschreiben!"
"filament_updated": "Dieses Filament wurde, seit dem die Seite geöffnet wurde, von jemand anderem/etwas anderem bearbeitet. Speichern wird diese Änderungen überschreiben!",
"import_external": "Importieren von extern",
"import_external_description": "Wählen Sie ein Filament in der Liste aus, um dessen Details automatisch in das Formular zur Filamenterstellung einzutragen. Dadurch werden alle Daten, die Sie in das Formular eingegeben haben, überschrieben.<br><br>Wenn Sie auf Ok klicken, wird automatisch ein Herstellerobjekt erstellt, falls erforderlich."
},
"buttons": {
"add_spool": "Spule hinzufügen"
}
},
"vendor": {
@@ -223,18 +266,23 @@
"id": "ID",
"name": "Name",
"registered": "Registriert",
"comment": "Kommentar"
"comment": "Kommentar",
"empty_spool_weight": "Gewicht der leeren Spule",
"external_id": "Externe ID"
},
"titles": {
"create": "Hersteller erstellen",
"clone": "Hersteller duplizieren",
"edit": "Hersteller bearbeiten",
"list": "Hersteller",
"list": "Herstellerliste",
"show": "Hersteller anzeigen",
"show_title": "[Marke #{{id}}] {{name}}"
"show_title": "[Hersteller #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "Diese Marke wurde, seit dem die Seite geöffnet wurde, von jemand anderem/etwas anderem bearbeitet. Speichern wird diese Änderungen überschreiben!"
"vendor_updated": "Dieser Hersteller wurde, seit dem die Seite geöffnet wurde, von jemand anderem/etwas anderem bearbeitet. Speichern wird diese Änderungen überschreiben!"
},
"fields_help": {
"empty_spool_weight": "Das Gewicht einer leeren Spule von diesem Hersteller."
}
},
"table": {
@@ -258,7 +306,7 @@
"clone": "#{{id}} Spule duplizieren | Spoolman"
},
"vendor": {
"list": "Hersteler | Spoolman",
"list": "Hersteller | Spoolman",
"show": "#{{id}} Hersteller anzeigen | Spoolman",
"edit": "#{{id}} Hersteller bearbeiten | Spoolman",
"create": "Herstellen erstellen | Spoolman",
@@ -298,15 +346,31 @@
"name": "Name",
"field_type": "Typ",
"unit": "Einheit",
"default_value": "Standardwert"
"default_value": "Standardwert",
"order": "Reihenfolge",
"multi_choice": "mehrfach Auswahl",
"key": "Schlüssel",
"choices": "Auswahlmöglichkeiten"
},
"field_type": {
"text": "Text",
"float": "Dezimalzahl"
"float": "Dezimalzahl",
"integer": "Ganze Zahl",
"choice": "Auswahl",
"integer_range": "Ganzzahlbereich",
"boolean": "Wahr/Falsch",
"float_range": "Float-Bereich",
"datetime": "Datum und Uhrzeit"
},
"boolean_true": "Ja",
"boolean_false": "Nein",
"delete_confirm": "Feld {{name}} entfernen?"
}
"delete_confirm": "Feld {{name}} entfernen?",
"description": "<p>Hier können Sie zusätzliche benutzerdefinierte Felder zu Ihren Elementen hinzufügen.</p><p>Nach dem Hinzufügen eines Feldes können Sie dessen Schlüssel oder Typ nicht mehr ändern, und bei Auswahlfeldern können Sie keine Auswahlmöglichkeiten entfernen oder den Mehrfachauswahlstatus ändern. Wenn Sie ein Feld entfernen, werden die zugehörigen Daten für alle Elemente gelöscht.</p><p>Der Schlüssel ist derjenige, unter dem andere Programme die Daten lesen/schreiben, wenn Ihr benutzerdefiniertes Feld also mit einem Programm eines Drittanbieters integriert werden soll, stellen Sie sicher, dass Sie ihn richtig einstellen. Der Standardwert wird nur auf neue Elemente angewendet.</p><p>Zusätzliche Felder können in den Tabellenansichten nicht sortiert oder gefiltert werden.</p>",
"choices_missing_error": "Du kannst die Auswahl nicht entfernen. Folgende Auswahlmöglichkeiten fehlen: {{choices}}",
"key_not_changed": "Bitte ändere den Schlüssel auf einen anderen Wert.",
"delete_confirm_description": "Dieses Feld und alle damit verbundenen Einträge werden gelöscht.",
"non_unique_key_error": "Der Schlüssel muss einzigartig sein."
},
"settings": "Einstellungen"
}
}

View File

@@ -110,7 +110,9 @@
"used_weight": "Χρησιμοποιημένο Βάρος",
"id": "Α/Α",
"remaining_weight": "Υπολειπόμενο Βάρος",
"price": ""
"price": "Τιμή",
"spool_weight": "Βάρος Κενού Καρουλιού",
"initial_weight": "Αρχικό Βάρος"
},
"titles": {
"archive": "Αρχειοθέτηση Καρουλιού",
@@ -132,7 +134,7 @@
"lot_nr": "Αριθμός παρτίδας κατασκευαστή. Μπορεί να χρησιμοποιηθεί για να διασφαλιστεί ότι μια εκτύπωση έχει σταθερό χρώμα εάν χρησιμοποιούνται πολλαπλά καρούλια.",
"remaining_weight": "Πόσο νήμα έχει απομείνει στο καρούλι. Για ένα καινούργιο καρούλι αυτό πρέπει να συμπίπτει με το βάρος του καρουλιού.",
"location": "Που βρίσκεται το καρούλι, εάν υπάρχουν πολλαπλά σημεία αποθήκευσης για τα καρούλια.",
"price": ""
"price": "Τιμή πλήρους καρουλιού. Εάν δεν οριστεί, η τιμή του νήματος θα θεωρηθεί αντ' αυτού."
},
"spool": "Καρούλια",
"messages": {
@@ -168,7 +170,7 @@
"name": "Όνομα νήματος, για να διακρίνει αυτόν τον τύπο νήματος μεταξύ άλλων από τον ίδιο προμηθευτή. Θα πρέπει να περιέχει το χρώμα για παράδειγμα.",
"article_number": "Π.χ. EAN, UPC, κλπ.",
"spool_weight": "Το βάρος ενός άδειου καρουλιού. Χρησιμοποιείται για τον προσδιορισμό του μετρημένου βάρους ενός καρουλιού.",
"price": "Τιμή ενός πλήρους καρουλιού στο νόμισμα που έχει διαμορφωθεί από το σύστημα.",
"price": "Τιμή ενός πλήρους καρουλιού.",
"weight": "Το βάρος του νήματος ενός πλήρους καρουλιού (καθαρό βάρος). Αυτό δεν πρέπει να περιλαμβάνει το βάρος του ίδιου του καρουλιού, μόνο το βάρος του νήματος. Είναι αυτό που συνήθως αναγράφεται στη συσκευασία.",
"material": "Π.χ. PLA, ABS, PETG, κλπ."
},
@@ -202,7 +204,10 @@
"form": {
"filament_updated": "Αυτό το νήμα έχει ενημερωθεί από κάποιον/κάτι άλλο από τότε που ανοίξατε αυτήν τη σελίδα. Η αποθήκευση θα αντικαταστήσει αυτές τις αλλαγές!"
},
"filament": "Νήματα"
"filament": "Νήματα",
"buttons": {
"add_spool": "Προσθήκη Καρουλιού"
}
},
"actions": {
"create": "Δημιουργία",
@@ -253,7 +258,9 @@
"success": "Επιτυχής",
"editSuccess": "Επιτυχής τροποποίηση του {{resource}}",
"deleteError": "Σφάλμα κατά τη διαγραφή του {{resource}} (status code: {{statusCode}})",
"createSuccess": "Δημιουργήθηκε επιτυχώς {{resource}}"
"createSuccess": "Δημιουργήθηκε επιτυχώς {{resource}}",
"saveSuccessful": "Επιτυχής Αποθήκευση!",
"validationError": "Κείμενο σφάλματος που εμφανίζεται εάν η φόρμα είχε προβλήματα κατά την υποβολή {{error}}"
},
"no": "Όχι",
"unknown": "Άγνωστος",
@@ -282,5 +289,46 @@
"kofi": "Φιλοδώρημα στο Ko-fi",
"home": {
"home": "Αρχική"
},
"settings": {
"extra_fields": {
"params": {
"default_value": "Προεπιλεγμένη τιμή",
"name": "Όνομα",
"field_type": "Τύπος",
"choices": "Επιλογές",
"multi_choice": "Πολλαπλή επιλογή",
"unit": "Μονάδα",
"order": "Σειρά",
"key": "Κλειδί"
},
"field_type": {
"float_range": "\"Float Range\" πληκτρολογήστε επιπλέον πεδίο, ένα εύρος δύο αριθμών με δεκαδικούς αριθμούς",
"choice": "\"Choice\" πληκτρολογήστε επιπλέον πεδίο, οι χρήστες μπορούν να επιλέξουν από μια λίστα τιμών",
"text": "Πληκτρολογήστε επιπλέον πεδίο \"Text\".",
"datetime": "Πληκτρολογήστε επιπλέον πεδίο \"Date+Time\"",
"boolean": "Πρόσθετο πεδίο τύπου \"Boolean\", Ναι/Όχι",
"integer": "\"Integer\" πληκτρολογήστε επιπλέον πεδίο, ακέραιο αριθμό χωρίς δεκαδικούς αριθμούς",
"float": "\"Float\" πληκτρολογήστε επιπλέον πεδίο, έναν αριθμό με δεκαδικούς αριθμούς",
"integer_range": "\"Integer Range\" πληκτρολογήστε επιπλέον πεδίο, ένα εύρος δύο ακέραιων αριθμών χωρίς δεκαδικoύς αριθμούς"
},
"non_unique_key_error": "Το κλειδί πρέπει να είναι μοναδικό.",
"key_not_changed": "Παρακαλώ αλλάξτε το κλειδί σε κάποιο άλλο.",
"delete_confirm": "Διαγραφή πεδίου {{name}};",
"tab": "Επιπλέον Πεδία",
"boolean_true": "Τιμή για ένα πρόσθετο πεδίο boolean Αληθές",
"boolean_false": "Τιμή για ένα πρόσθετο πεδίο boolean Λανθασμένο",
"choices_missing_error": "Δεν μπορείτε να αφαιρέσετε επιλογές. Λείπουν οι ακόλουθες επιλογές: {{choices}}",
"description": "<p>Εδώ μπορείτε να προσθέσετε επιπλέον προσαρμοσμένα πεδία στις οντότητες σας.</p><p>Μόλις προστεθεί ένα πεδίο, δεν μπορείτε να αλλάξετε το κλειδί ή τον τύπο του και για στα πεδία τύπου επιλογής δεν μπορείτε να αφαιρέσετε επιλογές ή να αλλάξετε την κατάσταση πολλαπλών επιλογών. Εάν καταργήσετε ένα πεδίο, τα συσχετισμένα δεδομένα για όλες τις οντότητες θα διαγραφούν.</p><p>Το κλειδί είναι πώς τα άλλα προγράμματα διαβάζουν/εγγράφουν τα δεδομένα, οπότε αν το προσαρμοσμένο πεδίο σας υποτίθεται ότι ενσωματώνεται με ένα πρόγραμμα τρίτου κατασκευαστή, φροντίστε να το ρυθμίσετε σωστά. Η προεπιλεγμένη τιμή εφαρμόζεται μόνο σε νέα στοιχεία.</p><p>Τα πρόσθετα πεδία δεν μπορούν να ταξινομηθούν ή να φιλτραριστούν στις προβολές πίνακα.</p>",
"delete_confirm_description": "Με αυτόν τον τρόπο θα διαγραφεί το πεδίο και όλα τα σχετικά δεδομένα για όλες τις οντότητες."
},
"general": {
"tab": "Γενικές",
"currency": {
"label": "Ετικέτα για τη ρύθμιση του νομίσματος"
}
},
"header": "Ρυθμίσεις",
"settings": "Ρυθμίσεις"
}
}

View File

@@ -15,6 +15,7 @@
"edit": "Edit",
"cancel": "Cancel",
"confirm": "Are you sure?",
"continue": "Continue",
"filter": "Filter",
"clear": "Clear",
"refresh": "Refresh",
@@ -72,6 +73,7 @@
"showBorder": "Show Border",
"previewScale": "Preview Scale",
"skipItems": "Skip Items",
"itemCopies": "Item Copies",
"contentSettings": "Content Settings",
"layoutSettings": "Layout Settings",
"horizontalSpacing": "Horizontal Spacing",
@@ -88,18 +90,24 @@
"none": "None",
"border": "Border",
"grid": "Grid"
}
},
"settings": "Presets",
"defaultSettings": "Default",
"addSettings": "Add New Preset",
"newSetting": "New",
"duplicateSettings": "Duplicate",
"deleteSettings": "Delete Current Preset",
"deleteSettingsConfirm": "Are you sure you want to delete this preset?",
"settingsName": "Preset Name",
"saveSetting": "Save Presets",
"saveAsImage": "Save as Image"
},
"qrcode": {
"button": "Print QR Codes",
"title": "QR Code Printing",
"spoolWeight": "Spool Weight: {{weight}}",
"lotNr": "Lot Nr: {{lot}}",
"bedTemp": "BT: {{temp}}",
"extruderTemp": "ET: {{temp}}",
"textSize": "Content Text Size",
"showSpoolmanIcon": "Show Spoolman Icon",
"showVendor": "Vendor",
"button": "Print Labels",
"title": "Label Printing",
"template": "Label Template",
"templateHelp": "Use {} to insert values of the spool object as text. For example {id} will be replaced with the spool id, or {filament.material} will be replaced with the material of the spool. Enclose text with double asterix ** to make it bold. Click the button to view a list of all available tags.",
"textSize": "Label Text Size",
"showContent": "Print Label",
"showLotNr": "Lot Nr",
"showSpoolWeight": "Spool Weight",
@@ -110,11 +118,17 @@
"useHTTPUrl": {
"label": "Use HTTP(S) link in QR code",
"tooltip": "Uses either the base URL specified in settings, or the current page URL if not set. If disabled will use proprietary link that will work only if scanned from Spoolman's scanning feature."
},
"showQRCode": "Print QR Code",
"showQRCodeMode": {
"no": "No",
"simple": "Simple",
"withIcon": "With Icon"
}
},
"spoolSelect": {
"title": "Select Spools",
"description": "Select spools to print QR codes for.",
"description": "Select spools to print labels for.",
"showArchived": "Show Archived",
"noSpoolsSelected": "You have not selected any spools.",
"selectAll": "Select/Unselect All",
@@ -140,6 +154,8 @@
"id": "ID",
"filament_name": "Filament",
"filament": "Filament",
"filament_internal": "Internal",
"filament_external": "External",
"price": "Price",
"material": "Material",
"weight_to_use": "Weight",
@@ -148,6 +164,8 @@
"measured_weight": "Measured Weight",
"used_length": "Used Length",
"remaining_length": "Remaining Length",
"initial_weight": "Initial Weight",
"spool_weight": "Empty Weight",
"location": "Location",
"lot_nr": "Lot Nr",
"first_used": "First Used",
@@ -162,8 +180,11 @@
"used_weight": "How much filament has been used from the spool. A new spool should have 0g used.",
"remaining_weight": "How much filament is left on the spool. For a new spool this should match the spool weight.",
"measured_weight": "How much the filament and spool weigh.",
"initial_weight": "The initial weight of filament on the spool (net weight). Will use the weight from the filament object if not set.",
"spool_weight": "The weight of the spool when it is empty. Leave empty to use the value from the filament or the manufacturer.",
"location": "Where the spool is located if you have multiple locations where you store your spools.",
"lot_nr": "Manufacturer's lot number. Can be used to ensure a print has consistent color if multiple spools are used."
"lot_nr": "Manufacturer's lot number. Can be used to ensure a print has consistent color if multiple spools are used.",
"external_filament": "You have selected a filament from the external database. A filament object (and possibly a manufacturer object) will be created automatically when you create this spool. This can create duplicate filament objects if you have already created a filament object for this filament."
},
"titles": {
"create": "Create Spool",
@@ -172,9 +193,17 @@
"list": "Spools",
"show": "Show Spool",
"show_title": "[Spool #{{id}}] {{name}}",
"archive": "Archive Spool"
"archive": "Archive Spool",
"adjust": "Adjust Spool Filament"
},
"form": {
"measurement_type": {
"length": "Length",
"weight": "Weight"
},
"measurement_type_label": "Measurement Type",
"adjust_filament_help": "Here you can directly add or subtract filament from the spool. A positive value will consume filament, a negative value will add it.",
"adjust_filament_value": "Consume Amount",
"new_location_prompt": "Enter a new location",
"spool_updated": "This spool has been updated by someone/something else since you opened this page. Saving will overwrite those changes!"
},
@@ -186,8 +215,8 @@
"filament": "Filaments",
"fields": {
"id": "ID",
"vendor_name": "Vendor",
"vendor": "Vendor",
"vendor_name": "Manufacturer",
"vendor": "Manufacturer",
"name": "Name",
"material": "Material",
"price": "Price",
@@ -201,15 +230,21 @@
"settings_extruder_temp": "Extruder Temp",
"settings_bed_temp": "Bed Temp",
"color_hex": "Color",
"single_color": "Single",
"multi_color": "Multi",
"coaxial": "Coextruded",
"longitudinal": "Longitudinal",
"external_id": "External ID",
"spools": "Show Spools"
},
"fields_help": {
"name": "Filament name, to distinguish this filament type among others from the same vendor. Should contain the color for example.",
"name": "Filament name, to distinguish this filament type among others from the same manufacturer. Should contain the color for example.",
"material": "E.g. PLA, ABS, PETG, etc.",
"price": "Price of a full spool.",
"weight": "The filament weight of a full spool (net weight). This should not include the weight of the spool itself, only the filament. It is what is usually written on the packaging.",
"spool_weight": "The weight of an empty spool. Used to determine measured weight of a spool.",
"article_number": "E.g. EAN, UPC, etc."
"article_number": "E.g. EAN, UPC, etc.",
"multi_color_direction": "Filaments can have multiple colors in two ways: either through coextrusion, like dual-color filaments with consistent multi-colors, or through longitudinal color changes, like gradient filaments that shift colors along the spool."
},
"titles": {
"create": "Create Filament",
@@ -220,30 +255,37 @@
"show_title": "[Filament #{{id}}] {{name}}"
},
"form": {
"filament_updated": "This filament has been updated by someone/something else since you opened this page. Saving will overwrite those changes!"
"filament_updated": "This filament has been updated by someone/something else since you opened this page. Saving will overwrite those changes!",
"import_external": "Import from External",
"import_external_description": "Select a filament in the list to automatically populate its details in the filament creation form. This will overwrite any data you have entered in the form.<br><br>A Manufacturer object will be created automatically when you click Ok, if necessary."
},
"buttons": {
"add_spool": "Add Spool"
}
},
"vendor": {
"vendor": "Vendors",
"vendor": "Manufacturers",
"fields": {
"id": "ID",
"name": "Name",
"empty_spool_weight": "Empty Spool Weight",
"external_id": "External ID",
"registered": "Registered",
"comment": "Comment"
},
"fields_help": {
"empty_spool_weight": "The weight of an empty spool from this manufacturer."
},
"titles": {
"create": "Create Vendor",
"clone": "Clone Vendor",
"edit": "Edit Vendor",
"list": "Vendors",
"show": "Show Vendor",
"show_title": "[Vendor #{{id}}] {{name}}"
"create": "Create Manufacturer",
"clone": "Clone Manufacturer",
"edit": "Edit Manufacturer",
"list": "Manufacturers",
"show": "Show Manufacturer",
"show_title": "[Manufacturer #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "This vendor has been updated by someone/something else since you opened this page. Saving will overwrite those changes!"
"vendor_updated": "This manufacturer has been updated by someone/something else since you opened this page. Saving will overwrite those changes!"
}
},
"home": {
@@ -256,7 +298,7 @@
"spool": "Individual physical spools of a specific filament.",
"vendor": "The companies that make the filament."
},
"description": "<title>Help</title><p>Here are some tips to get you started.</p><p>Spoolman holds 3 different types of data:</p><itemsHelp/><p>To add a new spool to the database, start by creating a <filamentCreateLink>Filament</filamentCreateLink> object. Once that's done, proceed to create a <spoolCreateLink>Spool</spoolCreateLink> object for that specific spool. If you acquire additional spools of the same filament later on, simply generate additional Spool objects and reuse the same Filament object.</p><p>Optionally, you can also generate a <vendorCreateLink>Vendor</vendorCreateLink> object for the company manufacturing the filament if you wish to track that information.</p><p>You have the option to link other 3D printer services to Spoolman, like Moonraker, which can automatically monitor filament usage and update the Spool objects for you. Refer to the <readmeLink>Spoolman README</readmeLink> for guidance on how to set that up.</p>"
"description": "<title>Help</title><p>Here are some tips to get you started.</p><p>Spoolman holds 3 different types of data:</p><itemsHelp/><p>To add a new spool to the database, start by creating a <filamentCreateLink>Filament</filamentCreateLink> object. Once that's done, proceed to create a <spoolCreateLink>Spool</spoolCreateLink> object for that specific spool. If you acquire additional spools of the same filament later on, simply generate additional Spool objects and reuse the same Filament object.</p><p>Optionally, you can also generate a <vendorCreateLink>Manufacturer</vendorCreateLink> object for the company manufacturing the filament if you wish to track that information.</p><p>You have the option to link other 3D printer services to Spoolman, like Moonraker, which can automatically monitor filament usage and update the Spool objects for you. Refer to the <readmeLink>Spoolman README</readmeLink> for guidance on how to set that up.</p>"
},
"table": {
"actions": "Actions"
@@ -330,11 +372,11 @@
"clone": "#{{id}} Clone Spool | Spoolman"
},
"vendor": {
"list": "Vendors | Spoolman",
"show": "#{{id}} Show Vendor | Spoolman",
"edit": "#{{id}} Edit Vendor | Spoolman",
"create": "Create Vendor | Spoolman",
"clone": "#{{id}} Clone Vendor | Spoolman"
"list": "Manufacturers | Spoolman",
"show": "#{{id}} Show Manufacturer | Spoolman",
"edit": "#{{id}} Edit Manufacturer | Spoolman",
"create": "Create Manufacturer | Spoolman",
"clone": "#{{id}} Clone Manufacturer | Spoolman"
}
}
}

View File

@@ -4,7 +4,7 @@
"create": "Crear",
"edit": "Editar",
"show": "Mostrar",
"clone": "Duplicar"
"clone": "Clonar"
},
"buttons": {
"create": "Crear",
@@ -13,10 +13,10 @@
"delete": "Borrar",
"edit": "Editar",
"cancel": "Cancelar",
"confirm": "Seguro?",
"confirm": "¿Estás seguro?",
"filter": "Filtrar",
"clear": "Limpiar",
"refresh": "Renovar",
"refresh": "Actualizar",
"show": "Mostrar",
"undo": "Deshacer",
"import": "Importar",
@@ -28,9 +28,10 @@
"notAccessTitle": "No tienes permiso de acceso",
"hideColumns": "Ocultar Columnas",
"clearFilters": "Borrar filtros",
"saveAndAdd": "Guardar y añadir"
"saveAndAdd": "Guardar y añadir",
"continue": "Continuar"
},
"warnWhenUnsavedChanges": "Seguro que quieres salir?. Hay cambios sin guardar.",
"warnWhenUnsavedChanges": "¿Estás seguro que quieres salir? Hay cambios sin guardar.",
"notifications": {
"success": "Éxito",
"error": "Error (status code: {{statusCode}})",
@@ -54,10 +55,10 @@
"clone": "Duplicar"
},
"dashboard": {
"title": "Panel"
"title": "Dashboard"
},
"spool": {
"spool": "Carretes",
"spool": "Bobinas",
"fields": {
"id": "ID",
"filament_name": "Filamento",
@@ -65,9 +66,9 @@
"material": "Material",
"used_weight": "Peso Usado",
"remaining_weight": "Peso Restante",
"used_length": "Largo Usado",
"remaining_length": "Largo Restante",
"location": "Locación",
"used_length": "Longitud usada",
"remaining_length": "Longitud restante",
"location": "Ubicación",
"lot_nr": "No. Lote",
"first_used": "Fecha de Primer Uso",
"last_used": "Ultimo Uso",
@@ -76,62 +77,83 @@
"archived": "Archivado",
"measured_weight": "Peso Medido",
"weight_to_use": "Peso",
"price": "Precio"
"price": "Precio",
"initial_weight": "Peso inicial",
"spool_weight": "Peso vacio",
"filament_internal": "Interno",
"filament_external": "Externo"
},
"fields_help": {
"used_weight": "Cuanto filamento ha sido usado en el carrete. Un carrete nuevo debería tener 0g usados.",
"location": "Donde esta el carrete si se guardan los carretes en lugares distintos.",
"lot_nr": "Numero de lote de manufactura. Puede ser usado si el modelo a imprimir necesita color homogéneo y varios carretes son necesarios.",
"weight_to_use": "Elegir el peso a usar. El peso medido solo esta disponible si existe el valor de peso del carrete seleccionado.",
"measured_weight": "Cuanto pesa el filamento y el carrete.",
"remaining_weight": "Cuanto filamento resta en el carrete. Para un carrete nuevo el peso debe ser igual al peso indicado de fabrica.",
"price": "Precio de una bobina completa en la moneda configurada en el sistema. Si no se establece, se asumirá en su lugar el precio del filamento."
"used_weight": "Cuanto filamento ha sido usado de la bobina. Una bobina nueva debería tener 0g usados.",
"location": "Donde se encuentra la bobina si tiene varias ubicaciones donde almacenar sus bobinas.",
"lot_nr": "Número de lote del fabricante. Puede ser usado si el modelo a imprimir necesita color homogéneo y varias bobinas son necesarias.",
"weight_to_use": "Elegir el peso a usar. El peso medido solo está disponible si existe el valor de peso del carrete seleccionado.",
"measured_weight": "Cuanto pesa el filamento y la bobina.",
"remaining_weight": "Cuanto filamento resta en la bobina. Para una bobina nueva el peso debe ser igual al peso indicado de fábrica.",
"price": "Precio de una bobina completa. Si no se establece, se asumirá en su lugar el precio del filamento.",
"initial_weight": "El peso inicial del filamento en el carrete (peso neto). Utilizará el peso del objeto de filamento si no está configurado.",
"spool_weight": "El peso de la bobina cuando está vacía. Dejar vacío para usar el valor del filamento o del fabricante.",
"external_filament": "Has elegido un filamento desde la base de datos externa. Un objeto filamento (y posiblemente un objeto fabricante) será creado automáticamente cuando crees este carrete. Esto puede crear objetos filamento duplicados si ya has creado un objeto filamento para este filamento."
},
"titles": {
"create": "Crear Carrete",
"clone": "Duplicar Carrete",
"edit": "Editar Carrete",
"list": "Carretes",
"show": "Mostrar Carrete",
"archive": "Archivar Carrete",
"show_title": "[Carrete #{{id}}] {{name}}"
"create": "Crear Bobina",
"clone": "Duplicar Bobina",
"edit": "Editar Bobina",
"list": "Bobinas",
"show": "Mostrar Bobina",
"archive": "Archivar Bobina",
"show_title": "[Bobina #{{id}}] {{name}}",
"adjust": "Ajustar la bobina del filamento"
},
"messages": {
"archive": "Esta seguro que quiere archivar este carrete?"
"archive": "¿Esta seguro que quiere archivar esta bobina?"
},
"form": {
"new_location_prompt": "Introduce una nueva ubicación",
"spool_updated": "Este spool ha sido actualizado por alguien/algo más desde que abriste esta página. ¡Al guardar, se sobrescribirán los cambios!"
"spool_updated": "Esta bobina ha sido actualizada por alguien/algo desde que abriste esta página. ¡Al guardar, se sobrescribirán los cambios!",
"measurement_type": {
"length": "Longitud",
"weight": "Peso"
},
"measurement_type_label": "Tipo de medición",
"adjust_filament_value": "Cantidad consumida",
"adjust_filament_help": "Aquí puedes añadir o restar directamente filamento de la bobina. Un valor positivo consumirá filamento, un valor negativo lo añadirá."
}
},
"filament": {
"filament": "Filamentos",
"fields": {
"id": "ID",
"vendor_name": "Marca",
"vendor": "Marca",
"vendor_name": "Fabricante",
"vendor": "Fabricante",
"name": "Nombre",
"material": "Material",
"price": "Precio",
"density": "Densidad",
"diameter": "Diámetro",
"weight": "Peso",
"spool_weight": "Peso del Carrete",
"spool_weight": "Peso de la Bobina",
"article_number": "Numero de Articulo",
"registered": "Registrado",
"comment": "Comentarios",
"settings_extruder_temp": "Temp. de Extrusor",
"settings_extruder_temp": "Temp. del Extrusor",
"settings_bed_temp": "Temp. de Cama",
"color_hex": "Color",
"spools": "Mostrar los carretes"
"spools": "Mostrar las Bobinas",
"external_id": "ID Externo",
"coaxial": "coextruido",
"single_color": "Único",
"longitudinal": "Longitudinal",
"multi_color": "Multiple"
},
"fields_help": {
"name": "Nombre del filamento, para distinguirlo de otros de la misma marca. Por ejemplo, debería contener el nombre del color.",
"name": "Nombre del filamento, para distinguir este tipo de filamento entre otros del mismo fabricante. Debe contener por ejemplo: el color.",
"material": "ej. PLA, ABS, PETG, etc.",
"price": "Precio del carrete completo en la divisa configurada en el sistema.",
"weight": "El peso de un carrete entero (peso neto). Esta cifra no debe incluir el peso del carrete vacío, solo el filamento. Es, usualmente, el peso escrito en la etiqueta del carrete.",
"spool_weight": "El peso de un carrete vacío. Se utiliza para determinar el peso medido de un carrete.",
"article_number": "ej. EAN, UPC, etc."
"price": "Precio de una bobina completa.",
"weight": "El peso de una bobina entera (peso neto). Esta cifra no debe incluir el peso de la bobina vacía, solo el filamento. Es, normalmente, el peso escrito en la etiqueta de la bobina.",
"spool_weight": "El peso de una bobina vacía. Se utiliza para determinar el peso medido de una bobina.",
"article_number": "ej. EAN, UPC, etc.",
"multi_color_direction": "Los filamentos pueden tener varios colores de dos formas: mediante coextrusión, como los filamentos bicolor con multicolores consistentes, o mediante cambios de color longitudinales, como los filamentos degradados que cambian de color a lo largo de la bobina."
},
"titles": {
"create": "Crear Filamento",
@@ -142,27 +164,37 @@
"show_title": "[Filamento #{{id}}] {{name}}"
},
"form": {
"filament_updated": "Este filamento ha sido actualizado por alguien/algo más desde que abrió esta página. ¡Al guardar, se sobrescribirán los cambios!"
"filament_updated": "Este filamento ha sido actualizado por alguien/algo más desde que abrió esta página. ¡Al guardar, se sobrescribirán los cambios!",
"import_external": "Importado desde un externo",
"import_external_description": "Seleccione un filamento en la lista para rellenar sus detalles de manera automática en el formulario de creación del filamento. Esto sobrescribirá cualquier información que hayas escrito en el formulario.<br><br>Un objeto fabricante será creado automáticamente cuando presiones Ok, si es necesario."
},
"buttons": {
"add_spool": "Añadir bobina"
}
},
"vendor": {
"vendor": "Marcas",
"vendor": "Fabricantes",
"fields": {
"id": "ID",
"name": "Nombre",
"registered": "Registrado",
"comment": "Comentarios"
"comment": "Comentarios",
"empty_spool_weight": "Peso de bobina vacía",
"external_id": "ID Externa"
},
"titles": {
"create": "Crear Marca",
"clone": "Duplicar Marca",
"edit": "Editar Marca",
"list": "Marcas",
"show": "Mostrar Marcas",
"show_title": "[Proveedor #{{id}}] {{name}}"
"create": "Crear un fabricante",
"clone": "Duplicar un fabricante",
"edit": "Editar un fabricante",
"list": "Fabricantes",
"show": "Mostrar Fabricante",
"show_title": "[Fabricante #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "Este vendedor ha sido actualizado por alguien/algo más desde que abrió esta página. ¡Al guardar, se sobrescribirán los cambios!"
"vendor_updated": "Este fabricante ha sido actualizado por alguien/algo desde que abrió esta página. ¡Al guardar, se sobrescribirán los cambios!"
},
"fields_help": {
"empty_spool_weight": "El peso de una bobina vacía de este fabricante."
}
},
"table": {
@@ -179,18 +211,18 @@
"clone": "#{{id}} Duplicar Filamento | Spoolman"
},
"spool": {
"list": "Carretes | Spoolman",
"show": "#{{id}} Mostrar Carrete | Spoolman",
"edit": "#{{id}} Editar Carrete | Spoolman",
"create": "Crear Carrete | Spoolman",
"clone": "#{{id}} Duplicar Carrete | Spoolman"
"list": "Bobinas | Spoolman",
"show": "#{{id}} Mostrar Bobina | Spoolman",
"edit": "#{{id}} Editar Bobina | Spoolman",
"create": "Crear Bobina | Spoolman",
"clone": "#{{id}} Duplicar Bobina | Spoolman"
},
"vendor": {
"list": "Marcas | Spoolman",
"show": "#{{id}} Mostrar Marca | Spoolman",
"edit": "#{{id}} Editar Marca | Spoolman",
"create": "Crear Marca | Spoolman",
"clone": "#{{id}} Duplicar Marca | Spoolman"
"list": "Fabricantes | Spoolman",
"show": "#{{id}} Mostrar el fabricante | Spoolman",
"edit": "#{{id}} Editar el fabricante | Spoolman",
"create": "Crear un fabricante | Spoolman",
"clone": "#{{id}} Duplicar un fabricante | Spoolman"
},
"home": {
"list": "Inicio | Spoolman"
@@ -202,21 +234,29 @@
"printing": {
"qrcode": {
"extruderTemp": "TE:{{temp}}",
"showSpoolWeight": "Peso Carrete",
"showSpoolWeight": "Peso de la Bobina",
"showContent": "Imprimir Etiqueta",
"showVendorComment": "Comentarios de Marca",
"spoolWeight": "Peso de Carrete: {{weight}}",
"showVendorComment": "Comentarios del Fabricante",
"spoolWeight": "Peso de la Bobina: {{weight}}",
"showTemperatures": "Temperaturas",
"showVendor": "Marca",
"showSpoolComment": "Comentario de Carrete",
"textSize": "Tamaño Letras de Contenido",
"title": "Impresión de Códigos QR",
"showVendor": "Fabricante",
"showSpoolComment": "Comentario de la Bobina",
"textSize": "Tamaño del texto para la etiqueta",
"title": "Imprimiendo las etiquetas",
"showSpoolmanIcon": "Mostrar Icono de Spoolman",
"showLotNr": "No. Lote",
"bedTemp": "TC:{{temp}}",
"lotNr": "No. Lote:{{lot}}",
"showFilamentComment": "Comentario de Filamento",
"button": "Imprimir Códigos QR"
"button": "Imprimir las etiquetas",
"template": "Plantilla para la etiqueta",
"templateHelp": "Utiliza {} para insertar valores del objeto spool como texto. Por ejemplo {id} será reemplazado por el id del spool, o {filament.material} será reemplazado por el material del spool. Encierra el texto con un doble asterisco ** para ponerlo en negrita. Haz clic en el botón para ver una lista de todas las etiquetas disponibles.",
"showQRCode": "Imprimir un código qr",
"showQRCodeMode": {
"no": "No",
"simple": "Sencilla",
"withIcon": "Con un icono"
}
},
"generic": {
"printerMarginBottom": "Zona Segura Inferior",
@@ -228,50 +268,61 @@
"border": "Borde"
},
"printerMarginTop": "Zona Segura Superior",
"customSize": "Tamaño Personal",
"customSize": "Personalizar",
"title": "Imprimiendo",
"contentSettings": "Ajustes de Contenido",
"marginRight": "Margen Derecho",
"printerMarginRight": "Zona Segura Derecha",
"helpPrinterMargin": "La Zona de Seguridad debe ajustarse lo mas cercano posible al borde que tu impresora es capaz de imprimir. Cambiar estos valores no afectaran todas las etiquetas.",
"helpPrinterMargin": "La Zona de Seguridad debe ajustarse lo más cercano posible al borde que tu impresora es capaz de imprimir. Cambiar estos valores no afectaran todas las etiquetas.",
"previewScale": "Previsualizar Escala",
"helpMargin": "Márgenes deben ser configurados a ser los mismos que tu papel e impresora. Cambiar los mismos afectaran todas las etiquetas.",
"helpMargin": "Los márgenes deben ser configurados a ser los mismos que tu papel e impresora. Cambiar los mismos afectaran todas las etiquetas.",
"marginTop": "Margen Superior",
"skipItems": "Saltar Elementos",
"layoutSettings": "Ajustes de Diseño",
"paperSize": "Tamaño de Papel",
"columns": "Columnas",
"description": "Ajustar los parámetros debajo para adquirir el diseño deseado. Recuerda que impresoras y el sistema operativo aplican sus propios márgenes y escala. Es probable que tengas que probar con valores varias veces hasta que el diseño te parezca correcto. Prueba en papel normal antes de usar etiquetas.",
"description": "Ajuste los parámetros siguientes para obtener el diseño de impresión deseado. Recuerda que impresoras y el sistema operativo aplican sus propios márgenes y escala. Es probable que tengas que probar con valores varias veces hasta que el diseño te parezca correcto. Prueba en papel normal antes de usar etiquetas.",
"marginBottom": "Margen Inferior",
"horizontalSpacing": "Espacio Horizontal",
"marginLeft": "Margen Izquierdo",
"showBorder": "Mostrar Borde",
"printerMarginLeft": "Zona Segura Izquierda",
"dimensions": "Dimensiones",
"print": "Imprimir"
"print": "Imprimir",
"itemCopies": "Copias del artículo",
"settings": "Ajustes preestablecidos",
"defaultSettings": "Por defecto",
"addSettings": "Añadir un nuevo ajustes preestablecidos",
"newSetting": "Nuevo",
"deleteSettings": "Eliminar ajustes preestablecidos",
"deleteSettingsConfirm": "¿Estás seguro de que deseas eliminar este ajuste preestablecido?",
"settingsName": "Nombre preestablecido",
"duplicateSettings": "Duplicar",
"saveSetting": "Guardar ajustes preestablecidos",
"saveAsImage": "Guardar como una imagen"
},
"spoolSelect": {
"selectedTotal_one": "{{count}} carrete seleccionado",
"selectedTotal_many": "{{count}} carretes seleccionados",
"selectedTotal_other": "{{count}} carretes seleccionados",
"description": "Seleccionar Carretes para imprimir códigos de QR.",
"showArchived": "Mostrar Archivados",
"title": "Selección de Carretes",
"selectedTotal_one": "{{count}} bobina seleccionada",
"selectedTotal_many": "{{count}} bobinas seleccionadas",
"selectedTotal_other": "{{count}} bobinas seleccionadas",
"description": "Selecciona los carretes para los que deseas imprimir las etiquetas.",
"showArchived": "Mostrar archivados",
"title": "Selección de Bobinas",
"selectAll": "Seleccionar/Deseleccionar Todos",
"noSpoolsSelected": "No has seleccionado ningún carrete."
"noSpoolsSelected": "No has seleccionado ninguna bobina."
}
},
"scanner": {
"title": "Escaner de Código QR",
"title": "Escáner de Código QR",
"error": {
"insecureContext": "La página no se sirve a través de HTTPS.",
"notReadable": "La camara no ha sido leída.",
"insecureContext": "La página no se muestra a través de HTTPS.",
"notReadable": "La cámara no es legible.",
"notAllowed": "No has dado permiso de acceso a la cámara.",
"unknown": "Un error desconocido ocurrió. ({{error}})",
"notFound": "La cámara no ha sido detectada.",
"streamApiNotSupported": "El navegador no es compatible con la API MediaStream."
},
"description": "Escanear códigos QR a Spoolman para ver detalles del carrete."
"description": "Escanea un código QR de Spoolman para ver detalles de la bobina."
},
"help": {
"resources": {
@@ -279,7 +330,7 @@
"filament": "Marcas de filamentos. Tienen propiedades como nombre, material, color, diámetro y más.",
"spool": "Bobinas físicas individuales de un filamento específico."
},
"description": "<title> Ayuda </title> <p> A continuación se ofrecen algunos consejos para empezar. </p> <p> Spoolman contiene 3 tipos diferentes de datos: </p> <itemsHelp/> <p> Para agregar un nuevo carrete a la base de datos, comience creando un objeto <filamentCreateLink> Filament </filamentCreateLink> . Una vez hecho esto, proceda a crear un objeto <spoolCreateLink> Spool </spoolCreateLink> para ese spool específico. Si adquiere carretes adicionales del mismo filamento más adelante, simplemente genere objetos Carrete adicionales y reutilice el mismo objeto Filamento. </p> <p> Opcionalmente, también puede generar un objeto <vendorCreateLink> Vendor </vendorCreateLink> para la empresa que fabrica el filamento si desea realizar un seguimiento de esa información. </p> <p> Tiene la opción de vincular otros servicios de impresora 3D a Spoolman, como Moonraker, que puede monitorear automáticamente el uso del filamento y actualizar los objetos de Spool por usted. Consulte el <readmeLink> Spoolman README </readmeLink> para obtener orientación sobre cómo configurarlo. </p>",
"description": "<title> Ayuda </title> <p> A continuación se ofrecen algunos consejos para empezar. </p> <p> Spoolman contiene 3 tipos diferentes de datos: </p> <itemsHelp/> <p> Para agregar una nueva bobina a la base de datos, comience creando un objeto <filamentCreateLink> Filamento </filamentCreateLink> . Una vez hecho esto, proceda a crear un objeto <spoolCreateLink> Bobina</spoolCreateLink> para esa bobina específica. Si adquiere bobinas adicionales del mismo filamento más adelante, simplemente genere objetos Bonina adicionales y reutilice el mismo objeto Filamento. </p> <p> Opcionalmente, también puede generar un objeto <vendorCreateLink> Fabricante</vendorCreateLink> para la empresa que fabrica el filamento si desea realizar un seguimiento de esa información. </p> <p> Tiene la opción de vincular otros servicios de impresora 3D a Spoolman, como Moonraker, que puede monitorear automáticamente el uso del filamento y actualizar los objetos de Bobina por usted. Consulte el <readmeLink> Spoolman README </readmeLink> para obtener orientación sobre cómo configurarlo. </p>",
"help": "Ayuda"
},
"kofi": "Donar en Ko-fi",
@@ -291,30 +342,30 @@
"general": {
"tab": "General",
"currency": {
"label": "Divisa"
"label": "Moneda"
}
},
"extra_fields": {
"tab": "Campos adicionales",
"params": {
"key": "Llave",
"key": "Clave",
"name": "Nombre",
"field_type": "Tipo",
"unit": "Unidad",
"order": "Orden",
"default_value": "Valor predeterminado",
"choices": "Elecciones",
"choices": "Opciones",
"multi_choice": "Múltiples opciones"
},
"field_type": {
"text": "Texto",
"integer": "Entero",
"integer_range": "Rango de enteros",
"float": "Flotante",
"float_range": "Rango flotante",
"float": "Decimal",
"float_range": "Rango Decimal",
"datetime": "Fecha y hora",
"boolean": "Booleano",
"choice": "Elección"
"choice": "Opción"
},
"boolean_true": "Sí",
"boolean_false": "No",
@@ -324,6 +375,7 @@
"delete_confirm": "¿Borrar campo {{name}}?",
"delete_confirm_description": "Esto eliminará el campo y todos los datos asociados para todas las entidades.",
"description": "<p> Aquí puede agregar más campos personalizados a los asuntos.</p><p> Una vez que se agrega un campo, no puede cambiar su clave o tipo, y no puede eliminar opciones ni cambiar el estado de múltiples opciones para campos seleccionados. Si elimina un campo, se eliminarán los datos asociados a todos los sujetos.</p><p> La forma en que otros programas leen/escriben los datos es clave, por lo que si su campo personalizado se va a integrar con un programa de terceros, asegúrese de que esté configurado correctamente. El valor predeterminado solo se utilizará para elementos nuevos.</p><p> Los campos de salida no se pueden ordenar ni filtrar en las vistas de tabla.</p>"
}
},
"settings": "Ajustes"
}
}

View File

@@ -0,0 +1,40 @@
{
"actions": {
"list": "Kuva",
"create": "Loo",
"edit": "Muuda",
"show": "Näita",
"clone": "Klooni"
},
"buttons": {
"create": "Loo",
"save": "Salvesta",
"confirm": "Oled kindel?",
"clear": "Tühjenda",
"refresh": "Uuenda",
"show": "Näita",
"hideColumns": "Peida tulbad",
"clearFilters": "Tühjenda filtrid",
"edit": "Muuda",
"undo": "Võta tagasi",
"clone": "Klooni",
"archive": "Arhiveeri",
"notAccessTitle": "Sul pole õigusi sisenemiseks",
"saveAndAdd": "Salvesta ja Lisa",
"logout": "Logi välja",
"delete": "Kustuta",
"cancel": "Tühista",
"import": "Impordi",
"filter": "Filtreeri",
"unArchive": "Võta arhiivist tagasi",
"hideArchived": "Peida arhiveeritud",
"showArchived": "Näita arhiveeritud"
},
"notifications": {
"error": "Viga (kood: {{statusCode}})",
"undoable": "Sul on {{seconds}} sekundit, et muudatus tagasi võtta.",
"success": "Õnnestunud",
"createSuccess": "Loodi {{resource}}"
},
"warnWhenUnsavedChanges": "Oled sa kindel, et soovid lahkuda? Sul on salvestamata muudatusi."
}

View File

@@ -4,19 +4,27 @@
"extruderTemp": "ET : {{temp}}",
"showSpoolWeight": "Poids de la bobine",
"showContent": "Imprimer l'étiquette",
"showVendorComment": "Commentaire sur le vendeur",
"showVendorComment": "Commentaire sur le fabricant",
"spoolWeight": "Poids de la bobine : {{weight}}",
"showTemperatures": "Températures",
"showVendor": "Vendeur",
"showVendor": "Fabricant",
"showSpoolComment": "Commentaire sur la bobine",
"textSize": "Contenu Taille du texte",
"title": "Impression de codes QR",
"textSize": "Taille du texte de l'étiquette",
"title": "Impression d'étiquettes",
"showSpoolmanIcon": "Montrer l'icône Spoolman",
"showLotNr": "N° de Lot",
"bedTemp": "BT : {{temp}}",
"lotNr": "N° de Lot : {{lot}}",
"showFilamentComment": "Commentaire sur le filament",
"button": "Imprimer des codes QR"
"button": "Imprimer des étiquettes",
"showQRCode": "Imprimer le QR Code",
"templateHelp": "Utilisez {} pour insérer les valeurs de l'objet bobine sous forme de texte. Par exemple, {id} sera remplacé par l'identifiant de la bobine ou {filament.material} sera remplacé par le matériau de la bobine. Entourez le texte d'un double astérisque ** pour le mettre en gras. Cliquez sur le bouton pour afficher une liste de toutes les balises disponibles.",
"template": "Modèle d'étiquette",
"showQRCodeMode": {
"no": "Non",
"simple": "Simple",
"withIcon": "Avec icône"
}
},
"generic": {
"printerMarginBottom": "Zone de sécurité en bas",
@@ -37,7 +45,7 @@
"previewScale": "Échelle de prévisualisation",
"helpMargin": "Les marges doivent être configurées en fonction de votre papier à étiquettes et de votre imprimante, car leur modification aura une incidence sur la taille de l'ensemble de la grille.",
"marginTop": "Marge supérieure",
"skipItems": "Passer les articles",
"skipItems": "Articles ignorés",
"layoutSettings": "Paramètres de mise en page",
"paperSize": "Format du papier",
"columns": "Colonnes",
@@ -48,13 +56,24 @@
"showBorder": "Afficher les bords",
"printerMarginLeft": "Zone de sécurité à gauche",
"dimensions": "Dimensions",
"print": "Imprimer"
"print": "Imprimer",
"settings": "Préréglages",
"defaultSettings": "Défaut",
"addSettings": "Ajouter un nouveau préréglage",
"newSetting": "Nouveau",
"deleteSettings": "Supprimer le préréglage actuel",
"deleteSettingsConfirm": "Êtes-vous sûr de vouloir supprimer ce préréglage?",
"settingsName": "Nom du préréglage",
"saveAsImage": "Enregistrer comme image",
"itemCopies": "Nombre d'article",
"duplicateSettings": "Dupliquer",
"saveSetting": "Enregistrer les préréglages"
},
"spoolSelect": {
"selectedTotal_one": "{{count}} bobine sélectionnée",
"selectedTotal_many": "{{count}} bobines sélectionnées",
"selectedTotal_other": "{{count}} bobines sélectionnées",
"description": "Sélectionnez les bobines pour lesquelles vous souhaitez imprimer des codes QR.",
"description": "Sélectionnez les bobines pour lesquelles imprimer des étiquettes.",
"showArchived": "Afficher les archives",
"title": "Sélectionner les bobines",
"selectAll": "Sélectionner/Désélectionner tout",
@@ -63,11 +82,11 @@
},
"documentTitle": {
"vendor": {
"create": "Créer le Vendeur | Spoolman",
"clone": "#{{id}} Cloner le Vendeur | Spoolman",
"edit": "#{{id}} Editer le Vendeur | Spoolman",
"list": "Vendeurs | Spoolman",
"show": "#{{id}} Afficher le vendeur | Spoolman"
"create": "Créer le fabricant | Spoolman",
"clone": "#{{id}} Cloner le fabricant | Spoolman",
"edit": "#{{id}} Éditer le fabricant| Spoolman",
"list": "Fabricants | Spoolman",
"show": "#{{id}} Afficher le fabricant | Spoolman"
},
"default": "Spoolman",
"spool": {
@@ -111,7 +130,11 @@
"used_weight": "Poids utilisé",
"id": "ID",
"remaining_weight": "Poids restant",
"price": "Prix"
"price": "Prix",
"initial_weight": "Poids initial",
"spool_weight": "Poids à vide",
"filament_internal": "Interne",
"filament_external": "Externe"
},
"titles": {
"archive": "Archiver la bobine",
@@ -120,7 +143,8 @@
"create": "Créer une bobine",
"clone": "Cloner une bobine",
"list": "Bobines",
"show_title": "[Bobine #{{id}}] {{name}}"
"show_title": "[Bobine #{{id}}] {{name}}",
"adjust": "Ajuster la bobine de filament"
},
"fields_help": {
"used_weight": "Combien de filament a été utilisé à partir de la bobine. Une bobine neuve devrait avoir 0g utilisé.",
@@ -129,7 +153,10 @@
"lot_nr": "Numéro de lot du fabricant. Peut être utilisé pour s'assurer que la couleur d'une impression est homogène si plusieurs bobines sont utilisées.",
"remaining_weight": "La quantité de filament restant sur la bobine. Pour une nouvelle bobine, cela devrait correspondre au poids de la bobine.",
"location": "L'emplacement de la bobine si vous stockez vos bobines à plusieurs endroits.",
"price": "Prix d'une bobine complète dans la devise configurée par le système. S'il n'est pas défini, le prix du filament sera pris en compte."
"price": "Prix d'une bobine complète. S'il n'est pas défini, le prix du filament sera pris en compte.",
"initial_weight": "Le poids initial du filament sur la bobine (poids net). Utilisera le poids de l'objet filament s'il n'est pas défini.",
"spool_weight": "Le poids de la bobine lorsqu'elle est vide. Laissez vide pour utiliser la valeur du filament ou du fabricant.",
"external_filament": "Vous avez sélectionné un filament dans la base de données externe. Un objet filament (et éventuellement un objet fabricant) sera créé automatiquement lorsque vous créerez cette bobine. Cela peut créer des objets filament en double si vous avez déjà créé un objet filament pour ce filament."
},
"spool": "Bobines",
"messages": {
@@ -137,7 +164,14 @@
},
"form": {
"spool_updated": "Cette bobine a été mise à jour par quelqu'un/quelque chose depuis la dernière ouverture de la page. Enregistrer écrasera ces changements !",
"new_location_prompt": "Entrer une nouvelle localisation"
"new_location_prompt": "Entrer une nouvelle localisation",
"measurement_type": {
"length": "Longueur",
"weight": "Poids"
},
"measurement_type_label": "Type de mesure",
"adjust_filament_help": "Ici, vous pouvez directement ajouter ou soustraire du filament à la bobine. Une valeur positive consommera du filament, une valeur négative en ajoutera.",
"adjust_filament_value": "Quantité consommée"
}
},
"buttons": {
@@ -162,20 +196,22 @@
"unArchive": "Désarchiver",
"clone": "Cloner",
"clear": "Effacer",
"saveAndAdd": "Sauvegarder et ajouter"
"saveAndAdd": "Sauvegarder et ajouter",
"continue": "Continuer"
},
"filament": {
"fields_help": {
"name": "Nom du filament, pour distinguer ce type de filament parmi d'autres du même fournisseur. Il doit contenir la couleur, par exemple.",
"name": "Nom du filament, pour distinguer ce type de filament parmi d'autres du même fabricant. Doit contenir la couleur par exemple.",
"article_number": "Exemple EAN, UPC, etc.",
"spool_weight": "Le poids d'une bobine vide. Utilisé pour déterminer le poids d'une bobine.",
"price": "Prix d'une bobine complète dans la devise configurée par le système.",
"price": "Prix d'une bobine complète.",
"weight": "Le poids du filament d'une bobine complète (poids net). Ce poids ne doit pas inclure le poids de la bobine elle-même, mais uniquement celui du filament. C'est ce qui est généralement indiqué sur l'emballage.",
"material": "Exemple PLA, ABS, PETG, etc."
"material": "Exemple PLA, ABS, PETG, etc.",
"multi_color_direction": "Les filaments peuvent avoir plusieurs couleurs de deux manières: soit par coextrusion, comme les filaments bicolores avec plusieurs couleurs uniformes, soit par des changements de couleur longitudinaux, comme les filaments dégradés qui changent de couleurs le long de la bobine."
},
"fields": {
"material": "Matériau",
"vendor_name": "Vendeur",
"vendor_name": "Fabricant",
"settings_extruder_temp": "Température de l'extrudeuse",
"comment": "Commentaire",
"diameter": "Diamètre",
@@ -185,12 +221,17 @@
"name": "Nom",
"price": "Prix",
"settings_bed_temp": "Température du lit",
"vendor": "Vendeur",
"vendor": "Fabricant",
"density": "Densité",
"id": "ID",
"color_hex": "Couleur",
"article_number": "Numéro d'article",
"spools": "Afficher les bobines"
"spools": "Afficher les bobines",
"external_id": "ID externe",
"single_color": "Unique",
"multi_color": "Multi",
"longitudinal": "Longitudinale",
"coaxial": "Coextrudée"
},
"titles": {
"clone": "Cloner un filament",
@@ -202,7 +243,12 @@
},
"filament": "Filaments",
"form": {
"filament_updated": "Ce filament a été mis à jour par quelqu'un/quelque chose depuis que vous avez ouvert cette page. Enregistrer écrasera ces changements !"
"filament_updated": "Ce filament a été mis à jour par quelqu'un/quelque chose depuis que vous avez ouvert cette page. Enregistrer écrasera ces changements !",
"import_external": "Importer depuis externe",
"import_external_description": "Sélectionnez un filament dans la liste pour renseigner automatiquement ses détails dans le formulaire de création de filament. Cela écrasera toutes les données que vous avez saisies dans le formulaire.<br><br>Un objet fabricant sera créé automatiquement lorsque vous cliquerez sur OK, si nécessaire."
},
"buttons": {
"add_spool": "Ajouter une bobine"
}
},
"actions": {
@@ -234,28 +280,35 @@
"success": "Réussite",
"editSuccess": "Édité avec succès {{resource}}",
"deleteError": "Erreur lors de la suppression de {{resource}} (status code : {{statusCode}})",
"createSuccess": "Création réussie de {{resource}}"
"createSuccess": "Création réussie de {{resource}}",
"saveSuccessful": "Sauvegarde réussie!",
"validationError": "Erreur de validation: {{error}}"
},
"no": "Non",
"unknown": "Inconnu",
"vendor": {
"titles": {
"show": "Afficher le vendeur",
"edit": "Editer un Vendeur",
"clone": "Cloner un Vendeur",
"create": "Créer un Vendeur",
"list": "Vendeurs",
"show_title": "[Vendeur #{{id}}] {{name}}"
"show": "Afficher le fabricant",
"edit": "Éditer un fabricant",
"clone": "Cloner un fabricant",
"create": "Créer un fabricant",
"list": "Fabricants",
"show_title": "[#{{id}} fabricant] {{name}}"
},
"fields": {
"registered": "Enregistré",
"comment": "Commentaire",
"name": "Nom",
"id": "ID"
"id": "ID",
"empty_spool_weight": "Poids de la bobine vide",
"external_id": "ID externe"
},
"vendor": "Vendeurs",
"vendor": "Fabricants",
"form": {
"vendor_updated": "Ce vendeur a été mis à jours par quelqu'un/quelque chose depuis que vous avez ouvert cette page. Enregistrer écrasera ces changements !"
"vendor_updated": "Ce fabricant a été mis à jours par quelqu'un/quelque chose depuis que vous avez ouvert cette page. Enregistrer écrasera ces changements!"
},
"fields_help": {
"empty_spool_weight": "Le poids d'une bobine vide de ce fabricant."
}
},
"yes": "Oui",
@@ -277,11 +330,52 @@
"filament": "Marques de filament. Contient les propriétés comme le nom, le matériau, la couleur, le diamètre, etc.",
"spool": "Les bobines physiques pour un filament spécifique."
},
"description": "<title>Aide</title><p>Voici quelques informations pour commencer.</p><p>Spoolman garde 3 types d'informations différentes:</p><itemsHelp/><p>Pour ajouter une nouvelle bobine dans la base de données, commencez par créer un objet <filamentCreateLink>Filament</filamentCreateLink>. Une fois cela fait, créez un objet <spoolCreateLink>bobine</spoolCreateLink> pour cette bobine spécifique. Si vous achetez plus tard des bobines supplémentaires du même filament, créez simplement un objet bobine supplémentaire et réutilisez le même objet Filament.</p><p>En option, vous pouvez créer un <vendorCreateLink>Vendeur</vendorCreateLink> pour la société qui fabrique le filament si vous voulez garder une trace de cette information.</p><p>Vous avez l'option de connecter d'autres service d'impression 3D à Spoolman, comme Moonraker, pour qu'ils puissent suivre automatiquement votre utilisation de filament et mettre automatiquement à jour l'objet Bobine pour vous. Voir le <readmeLink>README Spoolman</readmeLink> pour avoir la marche à suivre pour le mettre en place.</p>",
"description": "<title>Aide</title><p>Voici quelques informations pour commencer.</p><p>Spoolman garde 3 types d'informations différentes:</p><itemsHelp/><p>Pour ajouter une nouvelle bobine dans la base de données, commencez par créer un objet <filamentCreateLink>Filament</filamentCreateLink>. Une fois cela fait, créez un objet <spoolCreateLink>bobine</spoolCreateLink> pour cette bobine spécifique. Si vous achetez plus tard des bobines supplémentaires du même filament, créez simplement un objet bobine supplémentaire et réutilisez le même objet Filament.</p><p>En option, vous pouvez créer un <vendorCreateLink>Fabricant</vendorCreateLink> pour la société qui fabrique le filament si vous voulez garder une trace de cette information.</p><p>Vous avez l'option de connecter d'autres service d'impression 3D à Spoolman, comme Moonraker, pour qu'ils puissent suivre automatiquement votre utilisation de filament et mettre automatiquement à jour l'objet Bobine pour vous. Voir le <readmeLink>README Spoolman</readmeLink> pour avoir la marche à suivre pour le mettre en place.</p>",
"help": "Aide"
},
"kofi": "Me donner un pourboire sur Ko-fi",
"home": {
"home": "Accueil"
},
"settings": {
"settings": "Paramètres",
"extra_fields": {
"params": {
"choices": "Choix",
"multi_choice": "Choix multiple",
"default_value": "Valeur par défaut",
"key": "Clé",
"name": "Nom",
"field_type": "Type",
"unit": "Unité",
"order": "Ordre"
},
"field_type": {
"text": "Texte",
"choice": "Choix",
"integer": "Entier",
"float_range": "Plage décimal",
"datetime": "Date et heure",
"boolean": "Booléen",
"integer_range": "Plage d'entier",
"float": "Décimal"
},
"tab": "Champs additionnel",
"boolean_true": "Oui",
"boolean_false": "Non",
"non_unique_key_error": "La clé doit être unique.",
"delete_confirm_description": "Ceci supprimera le champ ainsi que toutes les données associées pour toutes les entités.",
"choices_missing_error": "Vous ne pouvez pas retirer les choix. Les choix suivants sont manquants: {{choices}}",
"key_not_changed": "Changer la clé pour quelque chose d'autre.",
"delete_confirm": "Supprimer le champ {{name}}?",
"description": "<p>Ici vous pouvez ajouter des champs personnalisés supplémentaires à vos entités.</p><p>Une fois qu'un champ est ajouté, vous ne pouvez plus changer sa clé ou son type, et pour les champs de types \"Choix\", vous ne pouvez plus retirer un choix ou changer l'état des choix multiples. Si vous supprimez un champ, les données associées aux entités seront supprimées.</p><p>La clé correspond à ce que les autres programmes lisent/écrivent, donc si votre champs doit s'intégrer avec un programme tiers, assurez-vous de le configurer correctement. La valeur par défaut n'est appliquées qu'aux nouveaux éléments.</p><p>Les champs supplementaires ne peuvent pas être triés ou filtrés dans les vues de table.</p>"
},
"header": "Paramètres",
"general": {
"currency": {
"label": "Devise"
},
"tab": "Général"
}
}
}

View File

@@ -85,10 +85,10 @@
},
"suffix": " | Spoolman",
"home": {
"list": ""
"list": "Kezdőlap | Spoolman"
},
"help": {
"list": ""
"list": "Segítség | Spoolman"
}
},
"spool": {
@@ -110,7 +110,7 @@
"used_weight": "Felhasznált súly",
"id": "ID",
"remaining_weight": "Hátralévő súly",
"price": ""
"price": "Ár"
},
"titles": {
"archive": "Arhivált tekercs",
@@ -128,7 +128,7 @@
"lot_nr": "Gyártói tételszám. A szín egységesség ellenőrzésére használható, ha több tekercset használsz egy nyomtatáshoz.",
"remaining_weight": "Mennyi filament van még a tekercsen. Egy új tekercsnél ez a tekercs súlyával kell megegyeznie.",
"location": "Hol található a tekercs, ha több helyen tárolod a tekercseidet.",
"price": ""
"price": "Egy teljes tekercs ára. Ha nincs beállítva, a rendszer az filament árát veszi fel helyette."
},
"form": {
"new_location_prompt": "Add meg az új helyet",
@@ -168,7 +168,7 @@
"name": "Filament neve, hogy megkülönböztesd ezt a filament típust másoktól ugyanattól a gyártótól. Tartalmazhatja például a színét.",
"article_number": "Például. EAN, UPC stb.",
"spool_weight": "Az üres tekercs súlya. Ezt használják a tekercs mért súlyának meghatározásához.",
"price": "A teljes tekercs ára a konfigurált pénznemben a rendszerben.",
"price": "A teljes tekercs ára.",
"weight": "A teljes tekercs súlya (nettó súly), amely nem tartalmazza a tekercs saját súlyát, csak a filament súlyát. Általában ez szerepel a csomagoláson.",
"material": "Például. PLA, ABS, PETG stb."
},
@@ -202,6 +202,9 @@
"filament": "Filamentek",
"form": {
"filament_updated": "Ez a szál anyag azóta frissült valaki/valami más által, mióta megnyitottad ezt az oldalt. A mentés felül fogja írni ezeket a változásokat!"
},
"buttons": {
"add_spool": "Tekercs hozzáadása"
}
},
"actions": {
@@ -253,7 +256,9 @@
"success": "Sikeres",
"editSuccess": "Sikeresen módosítva {{resource}}",
"deleteError": "Hiba a(z) {{resource}} törlésekor (hibakód: {{statusCode}})",
"createSuccess": "Sikeresen létrehozva {{resource}}"
"createSuccess": "Sikeresen létrehozva {{resource}}",
"saveSuccessful": "Sikeres mentés!",
"validationError": "Validációs hiba:{{error}}"
},
"no": "Nem",
"unknown": "Ismeretlen",
@@ -270,17 +275,58 @@
"tags": {
"clone": "Másolás"
},
"kofi": "",
"kofi": "Tipp a Ko-fi-ra",
"home": {
"home": ""
"home": "Kezdő lap"
},
"help": {
"resources": {
"spool": "",
"vendor": "",
"filament": ""
"spool": "Egyedi fizikai tekercs egy adott filamentből.",
"vendor": "Azok a cégek, amelyek gyártják a filamentet.",
"filament": "Filament márkája. Olyan tulajdonságokkal rendelkeznek, mint a név, az anyag, a szín, az átmérő stb."
},
"help": "",
"description": ""
"help": "Segítség",
"description": "<title>Segítség</title><p>Íme néhány tipp a kezdéshez.</p><p>A Spoolman 3 különböző adattípust tartalmaz:</p><itemsHelp/><p>Ha új tekercs szeretne hozzáadni az adatbázishoz, kezdje azzal, hogy létrehoz egy <filamentCreateLink>Filament</filamentCreateLink> objektumot. Amikor ezzel megvan, folytassa azzal, hogy létrehoz egy <spoolCreateLink>Tekercs</spoolCreateLink> objektumot az adott tekercs. Ha később további tekercsket szerez ugyanabból a filamentből, egyszerűen generáljon további Tekercs objektumokat, és újra használja ugyanazt a Filament objektumot.</p><p>Opcionálisan létrehozhat egy <vendorCreateLink>Gyártó</vendorCreateLink> objektumot is a filamentet gyártó vállalathoz, ha szeretné követni ezt az információt.</p><p>A Spoolmanhez más 3D-nyomtató szolgáltatásokat is csatlakoztathat, például a Moonrakert, amely automatikusan figyelemmel kíséri a filamenthasználatot, és frissíti az Tekercs objektumokat. Olvassa el a <readmeLink>Spoolman README</readmeLink> fájlt a beállításokkal kapcsolatos útmutatásért.</p>"
},
"settings": {
"settings": "Beállítások",
"extra_fields": {
"params": {
"choices": "Választás",
"multi_choice": "Több kiválasztása",
"key": "Kulcs",
"name": "Név",
"field_type": "Típus",
"unit": "Egység",
"order": "Rendezés",
"default_value": "Alapértelmezet érték"
},
"field_type": {
"text": "Szöveg",
"integer": "Egész szám",
"datetime": "Dátum és idő",
"float": "Lebegőpontos szám",
"integer_range": "Egész szám tartománya",
"float_range": "Lebegőpontos szám tartomány",
"choice": "Választás",
"boolean": "Logikai"
},
"key_not_changed": "Kérlek változtasd meg a kulcsot valami másra.",
"boolean_false": "Nem",
"tab": "Extra mezők",
"boolean_true": "Igen",
"choices_missing_error": "Nem tudod eltávolítani a választásokat. A következő választások hiányoznak: {{choices}}",
"non_unique_key_error": "A kulcsnak egyedinek kell lennie.",
"delete_confirm": "Töröli a következő mezőt: {{name}}?",
"delete_confirm_description": "Ez a mező törlése és az összes entitáshoz tartozó adat törlése.",
"description": "<p>Itt hozzáadhatsz további egyedi mezőket az entitásokhoz.</p><p>Ha egy mezőt hozzáadtál, nem lehet megváltoztatni annak kulcsát vagy típusát, és a választási típusú mezők esetében nem lehet eltávolítani a választásokat vagy megváltoztatni a többválasztásos állapotot. Ha eltávolítasz egy mezőt, az összes entitásra vonatkozó adat törlődik.</p><p>A kulcs az, amit más programok olvasnak/írnak az adatokként, tehát ha az egyedi meződ egy harmadik féllel integrálódik, győződj meg róla, hogy helyesen van beállítva. Az alapértelmezett érték csak az új elemekre vonatkozik.</p><p>Az extra mezőket nem lehet rendezni vagy szűrni a táblázatnézetekben.</p>"
},
"header": "Beállítások",
"general": {
"currency": {
"label": "Pénznem"
},
"tab": "Általános"
}
}
}

View File

@@ -16,7 +16,9 @@
"bedTemp": "BT: {{temp}}",
"lotNr": "Lotto Nr: {{lot}}",
"showFilamentComment": "Note Aggiuntive Filamento",
"button": "Stampa QR-Codes"
"button": "Stampa QR-Codes",
"template": "Modello",
"templateHelp": "Utilizza {} per inserire i valori della bobina nel testo. Ad esempio, {id} verrà sostituito con l'id della bobina, oppure {filament.material} verrà sostituito con il materiale della bobina. Racchiudi il testo con un doppio asterisco ** per renderlo in grassetto. Fai clic sul pulsante per visualizzare un elenco di tutte le etichette disponibili."
},
"generic": {
"printerMarginBottom": "Safe-Zone Inferiore",
@@ -48,7 +50,15 @@
"showBorder": "Mostra Bordi",
"printerMarginLeft": "Safe-Zone Sinistra",
"dimensions": "Dimensioni",
"print": "Stampa"
"print": "Stampa",
"settings": "Impostazioni",
"defaultSettings": "Predefinito",
"addSettings": "Aggiungi nuova impostazione",
"newSetting": "Nuovo",
"deleteSettings": "Elimina impostazione corrente",
"deleteSettingsConfirm": "Sei sicuro di voler eliminare questa impostazione?",
"settingsName": "Nome impostazione",
"itemCopies": "Copie dell'elemento"
},
"spoolSelect": {
"selectedTotal_one": "{{count}} bobina selezionata",
@@ -64,9 +74,9 @@
"documentTitle": {
"vendor": {
"create": "Crea Produttore | Spoolman",
"clone": "#{{id}} Clona Produttore | Spoolman",
"clone": "#{{id}} Duplica Produttore | Spoolman",
"edit": "#{{id}} Modifica Produttore | Spoolman",
"list": "Produttore | Spoolman",
"list": "Produttori | Spoolman",
"show": "#{{id}} Mostra Produttore | Spoolman"
},
"default": "Spoolman",
@@ -86,10 +96,10 @@
},
"suffix": " | Spoolman",
"home": {
"list": ""
"list": "Home | Spoolman"
},
"help": {
"list": ""
"list": "Aiuto | Spoolman"
}
},
"spool": {
@@ -111,7 +121,11 @@
"used_weight": "Peso Utilizzato",
"id": "ID",
"remaining_weight": "Peso Rimanente",
"price": ""
"price": "Prezzo",
"filament_external": "Esterno",
"filament_internal": "Interno",
"spool_weight": "Peso a vuoto/tara",
"initial_weight": "Peso iniziale"
},
"titles": {
"archive": "Archivia Bobina",
@@ -129,7 +143,10 @@
"lot_nr": "Numero di lotto del produttore. Può essere utilizzato per garantire che la stampa abbia colori uniformi se vengono utilizzate più bobine.",
"remaining_weight": "Quanto filamento è rimasto sulla bobina. Per una nuova bobina questo dovrebbe corrispondere al peso della bobina.",
"location": "Dove si trova la bobina se disponi di più posizioni in cui conservare le bobine.",
"price": ""
"price": "Prezzo di una bobina completa. Se non viene impostato, verrà utilizzato il prezzo del filamento.",
"spool_weight": "Il peso della bobina vuota (tara). Lasciare vuoto per usare il valore dal profilo filamento o dal profilo produttore.",
"external_filament": "Hai selezionato un filamento da un database esterno. Un profilo filamento (e possibilmente anche un profilo produttore) verrà creato automaticamente quando crei questa bobina. Ciò potrebbe creare un profilo filamento duplicato se hai già creato un profilo per questo filamento.",
"initial_weight": "Peso iniziale del filamento sulla bobina (peso netto). Se non impostato, viene utilizzato il peso dal profilo del filamento."
},
"form": {
"new_location_prompt": "Inserisci nuova posizione",
@@ -169,9 +186,10 @@
"name": "Nome del filamento, per distinguere questo tipo di filamento dagli altri dello stesso produttore. Dovrebbe contenere il colore, ad esempio.",
"article_number": "Es.: EAN, UPC, etc.",
"spool_weight": "Il peso di una bobina vuota. Utilizzato per determinare il peso misurato di una bobina.",
"price": "Prezzo di una bobina intera nella valuta configurata dal sistema.",
"price": "Prezzo di una bobina piena.",
"weight": "Il peso del filamento di una bobina piena (peso netto). Questo non dovrebbe includere il peso della bobina stessa, ma solo del filamento. È quello che solitamente c'è scritto sulla confezione.",
"material": "Es.: PLA, ABS, PETG, etc."
"material": "Es.: PLA, ABS, PETG, etc.",
"multi_color_direction": "I filamenti possono avere più colori in due modi: attraverso la coestrusione, come i filamenti a due colori con più colori omogenei, o attraverso cambiamenti di colore longitudinali, come i filamenti a gradiente che cambiano colore lungo la bobina."
},
"fields": {
"material": "Materiale",
@@ -184,13 +202,18 @@
"registered": "Registrato",
"name": "Nome",
"price": "Prezzo",
"settings_bed_temp": "Temperatura Bed",
"settings_bed_temp": "Temperatura Piatto",
"vendor": "Produttore",
"density": "Densità",
"id": "ID",
"color_hex": "Colore",
"article_number": "Numero Articolo",
"spools": "Mostra bobine"
"spools": "Mostra bobine",
"external_id": "ID esterno",
"single_color": "Unico",
"multi_color": "Multiplo",
"coaxial": "Coestruso",
"longitudinal": "Longitudinale"
},
"titles": {
"clone": "Clona Filamento",
@@ -202,7 +225,12 @@
},
"filament": "Filamenti",
"form": {
"filament_updated": "Ci sono stati degli aggiornamenti per questo filamento da quando è stata aperta questa pagina. Il salvataggio sovrascriverà le modifiche!"
"filament_updated": "Ci sono stati degli aggiornamenti per questo filamento da quando è stata aperta questa pagina. Il salvataggio sovrascriverà le modifiche!",
"import_external": "Importa da Esterno",
"import_external_description": "Seleziona un filamento dalla lista per compilare automaticamente i dettagli del modulo creazione filamento. Ciò sovrascriverà qualsiasi informazione hai precedentemente inserito nel modulo. <rb><br> Se necessario, verrà creato automaticamente un profilo Produttore quando clicchi Ok."
},
"buttons": {
"add_spool": "Aggiungi Bobina"
}
},
"actions": {
@@ -217,7 +245,7 @@
"show_title": "[Produttore #{{id}}] {{name}}",
"show": "Mostra Produttori",
"edit": "Modifica Produttore",
"clone": "Clona Produttore",
"clone": "Duplica Produttore",
"create": "Crea Produttore",
"list": "Produttori"
},
@@ -225,11 +253,16 @@
"registered": "Registrato",
"comment": "Note",
"name": "Nome",
"id": "ID"
"id": "ID",
"external_id": "ID Esterno",
"empty_spool_weight": "Peso a vuoto della bobina/tara"
},
"vendor": "Produttori",
"form": {
"vendor_updated": "Ci sono stati degli aggiornamenti per questo produttore da quando è stata aperta questa pagina. Il salvataggio sovrascriverà le modifiche!"
"vendor_updated": "Ci sono stati degli aggiornamenti per questo produttore da quando hai aperto questa pagina. Il salvataggio sovrascriverà le modifiche!"
},
"fields_help": {
"empty_spool_weight": "Il peso di una bobina vuota di questo produttore."
}
},
"scanner": {
@@ -254,7 +287,9 @@
"success": "Riuscito",
"editSuccess": "{{resource}} modificata con successo",
"deleteError": "Error nella cancellazione di {{resource}} (status code: {{statusCode}})",
"createSuccess": "{{resource}} creata correttamente"
"createSuccess": "{{resource}} creata correttamente",
"saveSuccessful": "Salvataggio completato!",
"validationError": "Errore di convalida: {{error}}"
},
"no": "No",
"unknown": "Sconosciuto",
@@ -272,7 +307,7 @@
"clone": "Clona"
},
"help": {
"description": "<title>Aiuto</title><p>Ecco alcuni suggerimenti per iniziare.</p><p>Spoolman può contenere 3 diversi tipi di dati:</p><itemsHelp/><p>Per aggiungere una nuova bobina al database, inizia creando una sezione<filamentCreateLink>Filamento</filamentCreateLink>. Fatto ciò, procedi con la creazione di una sezione <spoolCreateLink>Bobina</spoolCreateLink> per la questa specifica bobina. Se in seguito si acquisiscono altre bobine dello stesso filamento, sarà sufficiente generare altre sezioni bobina e riutilizzare la stessa sezione filamento.</p><p>Opzionalmente, si può anche creare una sezione <vendorCreateLink>Produttore</vendorCreateLink> per l'azienda produttrice del filamento, se si desidera tenere traccia di tali informazioni.</p><p>È possibile collegare a Spoolman altri servizi per stampanti 3D, come Moonraker, che può monitorare l'utilizzo del filamento e aggiornare le sezioni Spool automaticamente. Fare riferimento al <readmeLink>README di Spoolman</readmeLink> per maggiori informazioni sulla configurazione.</p>",
"description": "<title>Aiuto</title><p>Ecco alcuni suggerimenti per iniziare.</p><p>Spoolman può contenere 3 diversi tipi di dati:</p><itemsHelp/><p>Per aggiungere una nuova bobina al database, inizia creando una sezione <filamentCreateLink>filamento</filamentCreateLink>. Fatto ciò, procedi con la creazione di una sezione <spoolCreateLink>bobina</spoolCreateLink> per la questa specifica bobina. Se in seguito si acquisiscono altre bobine dello stesso filamento, sarà sufficiente generare altre sezioni bobina e riutilizzare la stessa sezione filamento.</p><p>Opzionalmente, si può anche creare una sezione <vendorCreateLink>produttore</vendorCreateLink> per l'azienda produttrice del filamento, se si desidera tenere traccia di tali informazioni.</p><p>È possibile collegare a Spoolman altri servizi per stampanti 3D, come Moonraker, che può monitorare l'utilizzo del filamento e aggiornare le sezioni Spool automaticamente. Fare riferimento al <readmeLink>README di Spoolman</readmeLink> per maggiori informazioni sulla configurazione.</p>",
"resources": {
"spool": "Bobine fisiche individuali di uno specifico filamento.",
"filament": "Marche di filamento. Hanno proprietà come nome, materiale, colore, diametro e altro.",
@@ -280,8 +315,49 @@
},
"help": "Aiuto"
},
"kofi": "",
"kofi": "Donazione su Ko-fi",
"home": {
"home": ""
"home": "Home"
},
"settings": {
"settings": "Impostazioni",
"extra_fields": {
"params": {
"order": "Ordine",
"default_value": "Valore di Default",
"key": "Chiave",
"name": "Nome",
"field_type": "Tipo",
"choices": "Scelte",
"multi_choice": "Scelta multipla",
"unit": "Unità"
},
"field_type": {
"text": "\"Testo\" per tipo campo extra",
"datetime": "Data e ora",
"choice": "Scelta",
"float": "Decimali",
"integer_range": "Intervallo di numeri interi",
"float_range": "Intervallo decimali",
"boolean": "Booleano",
"integer": "\"Intero\" per tipo campo extra, numero intero senza decimali"
},
"choices_missing_error": "Non è possibile rimuovere le scelte. Mancano i seguenti elementi: {{choices}}",
"non_unique_key_error": "La chiave deve essere univoca.",
"key_not_changed": "Si prega di utilizzare una chiave diversa.",
"boolean_true": "Si",
"boolean_false": "No",
"delete_confirm": "Cancellare il campo {{name}}?",
"delete_confirm_description": "Questo eliminerà il campo e tutti i dati associati di tutte le selezioni.",
"tab": "Campi extra",
"description": "<p>Qui è possibile aggiungere campi personalizzati.</p><p>Una volta aggiunto un campo, non è possibile modificarne la chiave o il tipo, e per i campi di tipo scelta non è possibile rimuovere le scelte o modificare lo stato di scelta multipla. Se si rimuove un campo, tutti i dati associati saranno cancellati.</p><p>La chiave è il modo in cui gli altri programmi leggono/scrivono i dati, quindi se il campo personalizzato deve integrarsi con un programma di terze parti, assicurarsi di impostarla correttamente. Il valore predefinito viene applicato solo ai nuovi elementi.</p><p>I campi extra non possono essere ordinati o filtrati nelle viste tabella.</p>"
},
"header": "Impostazioni",
"general": {
"tab": "Generale",
"currency": {
"label": "Valuta"
}
}
}
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -21,7 +21,8 @@
"filter": "Filtrer",
"refresh": "Gjenoppfrisk",
"notAccessTitle": "Du har ikke rettighet til å få tilgang",
"saveAndAdd": "Lagre og legg til"
"saveAndAdd": "Lagre og legg til",
"continue": "Fortsett"
},
"filament": {
"fields": {
@@ -95,25 +96,42 @@
"description": "Still inn innstillingene nedenfor for å få ønsket utskriftsoppsett. Husk at skrivere og operativsystemet ditt kan bruke sine egne marger og skalering, så det kan hende du må prøve og feile før alt er riktig. Test det på et stykke vanlig papir før du skriver ut på selve etikettene.",
"horizontalSpacing": "Horisontal avstand",
"marginLeft": "Venstre marg",
"showBorder": "Vis side"
"showBorder": "Vis side",
"defaultSettings": "Standard",
"newSetting": "Ny",
"addSettings": "Legg til ny mal",
"duplicateSettings": "Dupliser",
"deleteSettings": "Slett gjeldende mal",
"deleteSettingsConfirm": "Er du sikker på at du vil slette denne malen?",
"settingsName": "Navn på mal",
"saveSetting": "Lagre mal",
"settings": "Maler",
"saveAsImage": "Lagre som bilde"
},
"qrcode": {
"showTemperatures": "Temperaturer",
"button": "Skriv ut QR-koder",
"button": "Skriv ut etiketter",
"showVendorComment": "Leverandørkommentar",
"spoolWeight": "Spolevekt: {{weight}}",
"showVendor": "Leverandør",
"showSpoolComment": "Spolekommentar",
"title": "QR-kodeutskrift",
"title": "Etikettprinting",
"extruderTemp": "ET: {{temp}}",
"showSpoolWeight": "Spolevekt",
"showContent": "Skriv ut etikett",
"textSize": "Tekststørrelse på innhold",
"textSize": "Tekststørrelse på etikett",
"showSpoolmanIcon": "Vis Spoolman ikon",
"showLotNr": "Batch Nr",
"bedTemp": "BT: {{temp}}",
"lotNr": "Batch Nr: {{lot}}",
"showFilamentComment": "Filament kommentar"
"showFilamentComment": "Filament kommentar",
"template": "Etikett mal",
"showQRCodeMode": {
"simple": "Simpel",
"no": "Nei",
"withIcon": "Med ikon"
},
"showQRCode": "Print QR-kode"
},
"spoolSelect": {
"showArchived": "Vis arkiverte",
@@ -121,7 +139,7 @@
"noSpoolsSelected": "Du har ikke valgt noen spoler.",
"selectedTotal_one": "{{count}} spole valgt",
"selectedTotal_other": "{{count}} spoler valgt",
"description": "Velg spoler å printe ut QR-koder for.",
"description": "Velg spoler å printe ut etiketter for.",
"selectAll": "Velg/fjern merking av alle"
}
},
@@ -146,7 +164,11 @@
"filament_name": "Filament",
"filament": "Filament",
"location": "Lokasjon",
"price": ""
"price": "Pris",
"filament_internal": "Internt",
"filament_external": "Eksternt",
"initial_weight": "Startvekt",
"spool_weight": "Tom vekt"
},
"titles": {
"archive": "Arkiver spole",
@@ -168,7 +190,10 @@
"lot_nr": "Produsentens batch nummer. Kan bli benyttet or å verifiserer at print har en konsistent farge dersom flere spoler benyttes.",
"remaining_weight": "Hvor mye filament gjenstår på spolen. Ved en ny spole burde dette stemme med spolevekt.",
"location": "Hvor spolen er plassert hvis du har flere lokasjoner hvor du oppbevarer spoler.",
"price": ""
"price": "Pris på en full spole. Hvis ikke satt, vil prisen på filamentet bli antatt i stedet.",
"spool_weight": "Vekten av spolen når den er tom. La stå tomt for å bruke verdien fra filamentet eller produsenten.",
"initial_weight": "Startvekten av filament på spolen (nettovekt). Vil bruke vekten fra filament hvis ikke satt.",
"external_filament": "Du har valgt et filament fra den eksterne databasen. Et filamentobjekt (og muligens et produsentobjekt) opprettes automatisk når du oppretter denne spolen. Dette kan lage dupliserte filamentobjekter hvis du allerede har opprettet et filamentobjekt for denne filamentet."
},
"form": {
"new_location_prompt": "Skriv inn en ny lokasjon",
@@ -229,7 +254,9 @@
"createError": "Kunne ikke opprette {{resource}} (statuskode: {{statusCode}})",
"editSuccess": "{{resource}} redigert",
"deleteError": "Kunne ikke slette {{resource}} (statuskode: {{statusCode}})",
"createSuccess": "Opprettet {{resource}}"
"createSuccess": "Opprettet {{resource}}",
"saveSuccessful": "Lagring vellykket!",
"validationError": "Valideringsfeil: {{error}}"
},
"loading": "Laster",
"documentTitle": {
@@ -270,7 +297,7 @@
"dashboard": {
"title": "Oversikt"
},
"kofi": "",
"kofi": "Gi meg tips på Ko-fi",
"home": {
"home": ""
},

View File

@@ -9,19 +9,20 @@
"showArchived": "Gearchiveerd tonen",
"archive": "Archiveren",
"hideColumns": "Kolommen verbergen",
"confirm": "Weet je het zeker?",
"confirm": "Weet u het zeker?",
"delete": "Verwijderen",
"undo": "Ongedaan maken",
"import": "Importeren",
"logout": "Uitloggen",
"refresh": "Vernieuwen",
"notAccessTitle": "Je hebt geen toegang tot",
"notAccessTitle": "U heeft geen toegang tot",
"hideArchived": "Gearchiveerd verbergen",
"clearFilters": "Filters leegmaken",
"unArchive": "Dearchiveren",
"unArchive": "Terugzetten",
"clone": "Klonen",
"clear": "Wissen",
"saveAndAdd": "Opslaan en Toevoegen"
"saveAndAdd": "Opslaan en Toevoegen",
"continue": "Doorgaan"
},
"actions": {
"create": "Aanmaken",
@@ -39,10 +40,10 @@
"helpPrinterMargin": "Safe-Zone moet worden ingesteld op hoe dicht bij de papierrand uw printer kan afdrukken. Als u deze wijzigt, heeft dit geen invloed op het hele raster.",
"previewScale": "Voorbeeldschaal",
"helpMargin": "Marges moeten worden geconfigureerd zodat ze overeenkomen met je labelpapier en printer. Als u deze wijzigt, heeft dit invloed op de grootte van het hele raster.",
"skipItems": "Items overlaan",
"skipItems": "Items overslaan",
"paperSize": "Papierformaat",
"columns": "Kolommen",
"description": "Stem de onderstaande instellingen af om de gewenste printlay-out te krijgen. Houd er rekening mee dat printers en je besturingssysteem hun eigen marges en schaling kunnen toepassen, dus het kan zijn dat je met wat instellingen moet spelen voordat alles correct is. Test het op een stuk gewoon papier voordat u op de daadwerkelijke etiketten afdrukt.",
"description": "Stem de onderstaande instellingen af om de gewenste print lay-out te krijgen. Houd er rekening mee dat printers en je besturingssysteem hun eigen marges en schaling kunnen toepassen, dus het kan zijn dat je met wat instellingen moet spelen voordat alles correct is. Test het op een stuk gewoon papier voordat u op de daadwerkelijke etiketten afdrukt.",
"showBorder": "Rand tonen",
"dimensions": "Afmetingen",
"print": "Printen",
@@ -61,7 +62,18 @@
"marginBottom": "Onderste marge",
"horizontalSpacing": "Horizontale afstand",
"marginLeft": "Linker marge",
"printerMarginLeft": "Linker Safe-Zone"
"printerMarginLeft": "Linker Safe-Zone",
"settings": "Voorinstellingen",
"defaultSettings": "Standaard",
"addSettings": "Nieuwe voorinstelling toevoegen",
"newSetting": "Nieuw",
"deleteSettings": "Huidige voorinstelling wissen",
"deleteSettingsConfirm": "Weet je zeker dat je deze voorinstelling wilt verwijderen?",
"settingsName": "Voorinstellingsnaam",
"itemCopies": "Itemkopieën",
"duplicateSettings": "Dupliceren",
"saveSetting": "Voorinstelling opslaan",
"saveAsImage": "Opslaan als afbeelding"
},
"qrcode": {
"extruderTemp": "ET: {{temp}}",
@@ -72,30 +84,38 @@
"showTemperatures": "Temperaturen",
"showVendor": "Fabrikant",
"showSpoolComment": "Opmerking spoel",
"textSize": "Inhoud tekstgrootte",
"title": "QR-Code afdrukken",
"textSize": "Label tekstgrootte",
"title": "Label afdrukken",
"showSpoolmanIcon": "Spoolman icoon tonen",
"showLotNr": "Lotnr",
"showLotNr": "Lotnummer",
"bedTemp": "BT: {{temp}}",
"lotNr": "Lotnr: {{lot}}",
"lotNr": "Lotnummer: {{lot}}",
"showFilamentComment": "Opmerking filament",
"button": "QR-Codes afdrukken"
"button": "Labels afdrukken",
"template": "Label Sjabloon",
"templateHelp": "Gebruik {} om waarden van het spoelobject als tekst in te voegen. Bijvoorbeeld {id} wordt vervangen door het id van de spoel, of {filament.material} wordt vervangen door het materiaal van de spoel. Voeg een dubbel sterretje (**) toe voor en achter de tekst om het vet te maken. Klik op de knop om een lijst met alle beschikbare tags te bekijken.",
"showQRCodeMode": {
"no": "Nee",
"simple": "Simpel",
"withIcon": "Met Icoon"
},
"showQRCode": "Print QR-Code"
},
"spoolSelect": {
"selectedTotal_one": "{{count}} spoel geselecteerd",
"selectedTotal_other": "{{count}} spoelen geselecteerd",
"description": "Selecteer spoelen waarvoor je QR-codes wilt afdrukken.",
"description": "Selecteer spoelen waarvoor je labels wilt afdrukken.",
"showArchived": "Gearchiveerde tonen",
"title": "Spoelen selecteren",
"selectAll": "Alles selecteren/deselecteren",
"noSpoolsSelected": "Je hebt geen spoelen geselecteerd."
"selectAll": "Alles (de)selecteren",
"noSpoolsSelected": "U heeft geen spoelen geselecteerd."
}
},
"notifications": {
"deleteSuccess": "{{resource}} succesvol verwijderd",
"error": "Fout (fout code {{statusCode}})",
"editError": "Fout opgetreden bij het bewerken van {{resource}} (status code {{statusCode}})",
"undoable": "Je hebt {{seconds}} seconden om ongedaan te maken.",
"undoable": "U heeft {{seconds}} seconden om ongedaan te maken.",
"importProgress": "Importeren: {{processed}}/{{total}}",
"createError": "Er is een fout opgetreden bij het aanmaken van {{resource}} (status code {{statusCode}})",
"success": "Succesvol",
@@ -109,7 +129,7 @@
"unknown": "Onbekend",
"yes": "Ja",
"version": "Versie",
"warnWhenUnsavedChanges": "Weet je zeker dat je wil vertrekken? Er zijn niet-opgeslagen wijzigingen.",
"warnWhenUnsavedChanges": "Weet u zeker dat u deze pagina wilt verlaten? Er zijn niet-opgeslagen wijzigingen.",
"loading": "Laden",
"dashboard": {
"title": "Dashboard"
@@ -120,7 +140,7 @@
"documentTitle": {
"vendor": {
"create": "Fabrikant maken | Spoolman",
"clone": "#{{id}} Leverancier klonen | Spoolman",
"clone": "#{{id}} Fabrikant klonen | Spoolman",
"edit": "#{{id}} Fabrikant bewerken | Spoolman",
"list": "Fabrikanten | Spoolman",
"show": "#{{id}} Fabrikant tonen | Spoolman"
@@ -154,7 +174,7 @@
"last_used": "Laatst gebruikt",
"material": "Materiaal",
"weight_to_use": "Gewicht",
"lot_nr": "Lotnr",
"lot_nr": "Lotnummer",
"filament_name": "Filament",
"comment": "Opmerking",
"filament": "Filament",
@@ -167,7 +187,11 @@
"used_weight": "Gebruikt gewicht",
"id": "ID",
"remaining_weight": "Overgebleven gewicht",
"price": "Prijs"
"price": "Prijs",
"initial_weight": "Initieel gewicht",
"spool_weight": "Leeg gewicht",
"filament_internal": "Intern",
"filament_external": "Extern"
},
"titles": {
"archive": "Spoel archiveren",
@@ -176,34 +200,46 @@
"create": "Spoel maken",
"clone": "Spoel klonen",
"list": "Spoelen",
"show_title": "[Spoel #{{id}}] {{name}}"
"show_title": "[Spoel #{{id}}] {{name}}",
"adjust": "Spoel Filament aanpassen"
},
"fields_help": {
"used_weight": "Hoeveel filament er van de spoel is gebruikt. Op een nieuwe spoel zal 0g gebruikt zijn.",
"weight_to_use": "Selecteer welke gewichtswaarde je wilt invoeren. Het gemeten gewicht is alleen beschikbaar als het spoelgewicht is ingesteld voor het geselecteerde filament.",
"weight_to_use": "Selecteer welke gewichtswaarde u wilt invoeren. Het gemeten gewicht is alleen beschikbaar als het spoelgewicht is ingesteld voor het geselecteerde filament.",
"measured_weight": "Hoeveel het filament en de spoel wegen.",
"lot_nr": "Lotnummer van de fabrikant. Kan worden gebruikt om ervoor te zorgen dat een afdruk een consistente kleur heeft als er meerdere spoelen worden gebruikt.",
"remaining_weight": "Hoeveel filament zit er nog op de spoel. Bij een nieuwe spoel zal dit overeenkomen met het spoelgewicht.",
"location": "Waar de spoel zich bevindt als je meerdere locaties hebt waar je jouw spoelen opslaat.",
"price": "Prijs van een volledige spoel in de door het systeem geconfigureerde valuta. Als dit niet wordt ingevuld, wordt in plaats daarvan uitgegaan van de prijs van het filament."
"location": "Waar de spoel zich bevindt, wanneer er meerdere opslag locaties zijn.",
"price": "Prijs van een volledige spoel. Als dit niet wordt ingevuld, wordt in plaats daarvan uitgegaan van de prijs van het filament.",
"initial_weight": "Het initiële gewicht van het filament op de spoel (nettogewicht). Als dit niet is opgegeven, wordt het gewicht van het filament-object gebruikt.",
"spool_weight": "Het gewicht van de spoel als deze leeg is. Laat dit leeg om de waarde van het filament of de fabrikant te gebruiken.",
"external_filament": "U heeft een filament uit de externe database geselecteerd. Er wordt automatisch een filament-object (en mogelijk een fabrikant-object) gemaakt wanneer u deze spoel maakt. Hierdoor kunnen dubbele filament-objecten ontstaan als u al een filament-object voor dit filament hebt gemaakt."
},
"spool": "Spoelen",
"messages": {
"archive": "Weet je zeker dat je deze spoel wil archiveren?"
"archive": "Weet u zeker dat u deze spoel wil archiveren?"
},
"form": {
"new_location_prompt": "Voer een nieuwe locatie in",
"spool_updated": "Deze spoel is bijgewerkt door iemand/iets anders sinds je deze pagina hebt geopend. Opslaan zal die wijzigingen overschrijven!"
"spool_updated": "Deze spoel is bijgewerkt door iemand/iets anders sinds je deze pagina hebt geopend. Opslaan zal die wijzigingen overschrijven!",
"adjust_filament_help": "Hier kunt u direct filament toevoegen of verwijderen van de spoel. Een positieve waarde zal filament verbruiken, een negatieve waarde zal het toevoegen.",
"measurement_type": {
"length": "Lengte",
"weight": "Gewicht"
},
"measurement_type_label": "Meettype",
"adjust_filament_value": "Verbruikte hoeveelheid"
}
},
"filament": {
"fields_help": {
"name": "Filamentnaam, om dit filamenttype onder andere van dezelfde fabrikant te onderscheiden. Kan bijvoorbeeld de kleur bevatten.",
"name": "Filamentnaam, om dit filamenttype onder andere van dezelfde fabrikant te onderscheiden. Moet bijvoorbeeld de kleur bevatten.",
"article_number": "Bv. EAN. UPC, enz.",
"spool_weight": "Het gewicht van een lege spoel. Wordt gebruikt om het gemeten gewicht van een spoel te bepalen.",
"price": "Prijs van een volledige spoel in de door het systeem geconfigureerde valuta.",
"price": "Prijs van een volledige spoel.",
"weight": "Het filamentgewicht van een volle spoel (nettogewicht). Dit mag niet het gewicht van de spoel zelf omvatten, alleen het filament. Dit staat meestal op de verpakking.",
"material": "Bv. PLA, ABS, PETG, enz."
"material": "Bv. PLA, ABS, PETG, enz.",
"multi_color_direction": "Filamenten kunnen op twee manieren meerdere kleuren hebben: door coëxtrusie, zoals tweekleurige filamenten met consistente meerkleuren, of door kleurveranderingen in de lengterichting, zoals verloopfilamenten die van kleur veranderen langs de spoel."
},
"fields": {
"material": "Materiaal",
@@ -221,20 +257,27 @@
"density": "Dichtheid",
"id": "ID",
"color_hex": "Kleur",
"article_number": "Artiekelnummer",
"spools": "Spoelen tonen"
"article_number": "Artikel nummer",
"spools": "Spoelen tonen",
"external_id": "Extern ID",
"single_color": "Enkel",
"multi_color": "Meervoudig",
"coaxial": "Gecoëxtrudeerde",
"longitudinal": "Longitudinaal"
},
"titles": {
"clone": "Flimanent klonen",
"clone": "Filament klonen",
"show": "Filament tonen",
"create": "Flimament maken",
"create": "Filament maken",
"edit": "Filament bewerken",
"list": "Filamenten",
"show_title": "[Filament #{{id}}] {{name}}"
},
"filament": "Filamenten",
"form": {
"filament_updated": "Dit filament is bijgewerkt door iemand/iets anders sinds je deze pagina geopend hebt. Deze veranderingen worden overschreven als je op slaat!"
"filament_updated": "Dit filament is bijgewerkt door iemand/iets anders sinds je deze pagina geopend hebt. Deze veranderingen worden overschreven als je op slaat!",
"import_external_description": "Selecteer een filament in de lijst om de details ervan automatisch in te vullen in het formulier voor het maken van filamenten. Hierdoor worden alle gegevens die u in het formulier hebt ingevoerd, overschreven.<br><br>Er wordt indien nodig automatisch een Fabrikant-object gemaakt als u op OK klikt.",
"import_external": "Importeren van extern"
},
"buttons": {
"add_spool": "Spoel toevoegen"
@@ -265,15 +308,20 @@
"registered": "Geregistreerd",
"comment": "Opmerking",
"name": "Naam",
"id": "ID"
"id": "ID",
"empty_spool_weight": "Gewicht lege spoel",
"external_id": "Extern ID"
},
"vendor": "Fabrikanten",
"form": {
"vendor_updated": "Deze leverancier is bijgewerkt dor iemand/iets anders sinds je deze pagina geopend hebt. De veranderingen worden overschreven als je opslaat!"
"vendor_updated": "Deze fabrikant is bijgewerkt door iemand/iets anders sinds je deze pagina geopend hebt. De veranderingen worden overschreven als je opslaat!"
},
"fields_help": {
"empty_spool_weight": "Het gewicht van een lege spoel van deze fabrikant."
}
},
"table": {
"actions": "Akties"
"actions": "Acties"
},
"help": {
"resources": {
@@ -281,7 +329,7 @@
"filament": "Merken filament. Ze hebben eigenschappen zoals naam, materiaal, kleur, diameter en meer.",
"spool": "Individuele fysieke spoelen van een specifiek filament."
},
"description": "<title>Help</title><p>Hier zijn enkele tips om u op weg te helpen.</p><p>Spoolman bewaart 3 verschillende soorten gegevens:</p><itemsHelp/><p>Om een nieuwe toe spoel toe te voegen in de database, begin dan met het maken van een <filamentCreateLink>Filament</filamentCreateLink> object. Zodra dat is gebeurd, gaat u verder met het maken van een <spoolCreateLink>Spoel</spoolCreateLink>-object voor die specifieke spoel. Als u later extra spoelen van hetzelfde filament aanschaft, genereert u eenvoudigweg extra Spoel-objecten en hergebruikt u hetzelfde Filament-object.</p><p>Optioneel kunt u ook een <vendorCreateLink>Vendor</vendorCreateLink>-object voor het bedrijf genereren als u die informatie wilt bijhouden.</p><p>U heeft de mogelijkheid om andere 3D-printerservices aan Spoolman te koppelen, zoals Moonraker, die automatisch het filament gebruik kan monitoren en de Spoel-objecten voor u kan bijwerken. Raadpleeg de <readmeLink>Spoolman README</readmeLink> voor richtlijnen over hoe u dat kunt instellen.</p>",
"description": "<title>Help</title><p>Hier zijn enkele tips om u op weg te helpen.</p><p>Spoolman bewaart 3 verschillende soorten gegevens:</p><itemsHelp/><p>Om een nieuwe toe spoel toe te voegen in de database, begin dan met het maken van een <filamentCreateLink>Filament</filamentCreateLink> object. Zodra dat is gebeurd, gaat u verder met het maken van een <spoolCreateLink>Spoel</spoolCreateLink>-object voor die specifieke spoel. Als u later extra spoelen van hetzelfde filament aanschaft, genereert u eenvoudigweg extra Spoel-objecten en hergebruikt u hetzelfde Filament-object.</p><p>Optioneel kunt u ook een <vendorCreateLink>Fabrikant</vendorCreateLink>-object voor het bedrijf genereren als u die informatie wilt bijhouden.</p><p>U heeft de mogelijkheid om andere 3D-printerservices aan Spoolman te koppelen, zoals Moonraker, die automatisch het filament gebruik kan monitoren en de Spoel-objecten voor u kan bijwerken. Raadpleeg de <readmeLink>Spoolman README</readmeLink> voor richtlijnen over hoe u dat kunt instellen.</p>",
"help": "Help"
},
"kofi": "Tip mij op Ko-fi",
@@ -322,10 +370,11 @@
"boolean_false": "Nee",
"choices_missing_error": "U kunt geen keuzes verwijderen. De volgende keuzes ontbreken: {{choices}}",
"non_unique_key_error": "De key moet uniek zijn.",
"key_not_changed": "Verander de key in iets anders.",
"key_not_changed": "Key niet gewijzigd.",
"delete_confirm": "Veld {{name}} verwijderen?",
"delete_confirm_description": "Hierdoor worden het veld en alle bijbehorende gegevens voor alle entiteiten verwijderd.",
"description": "<p>Hier kunt u extra aangepaste velden toevoegen aan uw entiteiten.</p><p>Zodra een veld is toegevoegd, kunt u de key of het type ervan niet wijzigen, en voor velden met keuzetypes kunt u geen keuzes verwijderen of de meervoudige velden wijzigen. Als u een veld verwijdert, worden de bijbehorende gegevens voor alle entiteiten verwijderd.</p><p>De key is hoe andere programma's de gegevens lezen/schrijven, dus of uw aangepaste veld moet worden geïntegreerd met een derde partij programma, zorg ervoor dat u het correct instelt. De standaardwaarde wordt alleen toegepast op nieuwe items.</p><p>Extra velden kunnen niet worden gesorteerd of gefilterd in de tabelweergaven.</p>"
}
},
"settings": "Instellingen"
}
}

View File

@@ -47,7 +47,7 @@
"filament": "Filament",
"material": "Materiał",
"remaining_weight": "Pozostała waga",
"used_length": "Użyta długość",
"used_length": "Zużyta długość",
"remaining_length": "Pozostała długość",
"location": "Lokalizacja",
"lot_nr": "Numer partii (LOT)",
@@ -55,11 +55,13 @@
"last_used": "Ostatnie użycie",
"comment": "Komentarz",
"archived": "Zarchiwizowano",
"used_weight": "Użyta waga",
"used_weight": "Zużyta waga",
"registered": "Zarejestrowano",
"measured_weight": "Zmierzona waga",
"weight_to_use": "Waga",
"price": ""
"price": "Cena",
"initial_weight": "Waga początkowa",
"spool_weight": "Waga pusta"
},
"fields_help": {
"location": "Wpisz gdzie znajduje się szpula, jeśli masz wiele lokalizacji, w których przechowujesz szpule.",
@@ -68,7 +70,9 @@
"remaining_weight": "Ile filamentu pozostało na szpuli. W przypadku nowej szpuli powinno to odpowiadać wadze szpuli.",
"measured_weight": "Waga filamentu i szpuli.",
"weight_to_use": "Wybierz jaką wagę wprowadzić. Zmierzona waga jest dostępna tylko wtedy, gdy waga szpuli jest ustawiona dla wybranego filamentu.",
"price": ""
"price": "Cena pełnej szpuli. Jeśli nie ustawiona, cena filamentu będzie domyślna.",
"initial_weight": "Masa początkowa filamentu(netto). Może być podana przez producenta, ale możesz ustawić dla każdej szpuli, aby uwzględnić ewentualne niedokładności.",
"spool_weight": "Waga pustej szpuli. Może być podana przez producenta, ale możesz ją ustawić osobno dla każdej szpuli, aby uwzględnić niedokładności."
},
"messages": {
"archive": "Czy na pewno chcesz zarchiwizować tę szpulę?"
@@ -91,7 +95,9 @@
"editError": "Błąd podczas edycji {{resource}} (kod statusu: {{statusCode}})",
"importProgress": "Importowanie: {{processed}}/{{total}}",
"error": "Błąd (kod statusu: {{statusCode}})",
"createError": "Wystąpił błąd podczas tworzenia {{resource}} (kod statusu: {{statusCode}})"
"createError": "Wystąpił błąd podczas tworzenia {{resource}} (kod statusu: {{statusCode}})",
"saveSuccessful": "Zapisano pomyślnie!",
"validationError": "Błąd walidacji: {{error}}"
},
"loading": "Wczytywanie",
"version": "Wersja",
@@ -124,7 +130,7 @@
"marginBottom": "Dolny margines",
"printerMarginLeft": "Bezpieczna strefa po lewej",
"printerMarginRight": "Bezpieczna strefa po prawej",
"printerMarginTop": "Bezpieczna strefa u góry",
"printerMarginTop": "Bezpieczna strefa od góry",
"borders": {
"none": "Brak",
"border": "Obwódka",
@@ -132,7 +138,7 @@
},
"description": "Dostosuj poniższe ustawienia, aby uzyskać pożądany układ wydruku. Należy pamiętać, że drukarki i system operacyjny mogą stosować własne marginesy i skalowanie, więc może być konieczne wykonanie kilku prób i błędów, zanim wszystko będzie prawidłowe. Przed wydrukowaniem etykiet przetestuj je na zwykłym papierze.",
"horizontalSpacing": "Poziomy odstęp",
"printerMarginBottom": "Bezpieczna strefa u dołu"
"printerMarginBottom": "Bezpieczna strefa od dołu"
},
"qrcode": {
"button": "Drukuj kody QR",
@@ -198,7 +204,7 @@
},
"fields_help": {
"material": "Np. PLA, ABS, PETG itp.",
"price": "Cena pełnej szpuli w walucie skonfigurowanej przez system.",
"price": "Cena pełnej szpuli.",
"spool_weight": "Waga pustej szpuli. Używana do określenia ciężaru szpuli.",
"article_number": "Np. EAN, UPC itp.",
"name": "Nazwa filamentu, aby odróżnić ten typ filamentu od innych pochodzących od tego samego producenta. Powinna zawierać np. kolor.",
@@ -214,6 +220,9 @@
},
"form": {
"filament_updated": "Ten filament został zaktualizowany przez kogoś/coś innego od czasu otwarcia tej strony. Zapisanie nadpisze te zmiany!"
},
"buttons": {
"add_spool": "Dodaj Szpulę"
}
},
"vendor": {
@@ -222,7 +231,8 @@
"id": "ID",
"name": "Nazwa",
"registered": "Zarejestrowano",
"comment": "Komentarz"
"comment": "Komentarz",
"empty_spool_weight": "Waga pustej szpuli"
},
"titles": {
"create": "Tworzenie producenta",
@@ -234,6 +244,9 @@
},
"form": {
"vendor_updated": "Ten producent został zaktualizowany przez kogoś/coś innego od czasu otwarcia tej strony. Zapisanie spowoduje nadpisanie tych zmian!"
},
"fields_help": {
"empty_spool_weight": "Waga pustej szpuli od tego producenta."
}
},
"table": {
@@ -277,10 +290,52 @@
"filament": "Producenci filamentów. Mają takie właściwości, jak nazwa, materiał, kolor, średnica i inne.",
"spool": "Indywidualne fizyczne szpule określonego filamentu."
},
"help": "Pomoc"
"help": "Pomoc",
"description": "<title>Pomoc</title><p>Oto kilka wskazówek na początek.</p><p>Spoolman przechowuje 3 różne typy danych:</p><itemsHelp/><p>Aby dodać nową szpulę do bazy danych, zacznij od utworzenia obiektu <filamentCreateLink>Filament</filamentCreateLink>. Gdy już to zrobisz, przejdź do utworzenia obiektu <spoolCreateLink>Szpula</spoolCreateLink> dla tej konkretnej szpuli. Jeśli później zdobędziesz dodatkowe szpule tego samego filamentu, po prostu wygeneruj dodatkowe obiekty Szpula i ponownie użyj tego samego obiektu Filament.</p><p>Opcjonalnie możesz także wygenerować obiekt <vendorCreateLink>Producent</vendorCreateLink> dla firmy produkującej filament, jeśli chcesz śledzić te informacje.</p><p>Masz możliwość połączenia innych usług drukarek 3D ze Spoolmanem, takich jak Moonraker, który może automatycznie monitorować zużycie filamentu i aktualizować obiekty Szpula. Wskazówki dotyczące konfiguracji można znaleźć w <readmeLink>Spoolman README</readmeLink>.</p>"
},
"kofi": "Daj napiwek na Ko-fi",
"home": {
"home": "Strona główna"
},
"settings": {
"settings": "Ustawienia",
"general": {
"currency": {
"label": "Waluta"
},
"tab": "Ogólne"
},
"extra_fields": {
"tab": "Dodatkowe Parametry",
"boolean_true": "Tak",
"boolean_false": "Nie",
"params": {
"name": "Nazwa",
"field_type": "Typ",
"unit": "Jednostka",
"order": "Kolejność",
"default_value": "Domyślna Wartość",
"key": "Klucz",
"choices": "Wybory",
"multi_choice": "Wielokrotny wybór"
},
"field_type": {
"integer": "Liczba całkowita",
"integer_range": "Zakres liczb",
"float": "Liczba (ułamek)",
"choice": "Wybór",
"text": "Tekst",
"float_range": "Zakres",
"datetime": "Data i Czas",
"boolean": "Wartość logiczna"
},
"non_unique_key_error": "Klucz musi być unikalny.",
"description": "<p>Tutaj możesz dodać dodatkowe komórki do zapisu danych.</p><p>Kiedy dodasz komórkę, nie można już zmienić typu danych ani klucza, a dla komurek wyboru, nie możesz zmienić możliwych wyborów ani multi-wybierania. Jeśli usuniesz komórkę, wszystkie informacje ze wszystkich obiektów z tej komórki zostaną usunięte.</p><p>Klucz jest po to, aby umożliwić innym programom odczytywać/zapisywac swoje dane,więc jeśli twoje nowa customowa komórka ma służyć do integracji z third-party programami, upewnij się że jest dobrze skonfigurowane. Domyślna wartość jest dodawana tylko do nowych komórek.</p><p>Dodatkowe komórki nie moga być sortowane ani filtrowane.</p>",
"choices_missing_error": "Nie możesz usunąc możliwośći wyboru. Ten wybór nie istnieje: {{choices}}",
"key_not_changed": "Zmień klucz na inny.",
"delete_confirm": "Usunąć pole {{name}}?",
"delete_confirm_description": "Ta akcja usunie wszystkie pola i powiązane z nimi dane dla wszystkich obiektów."
},
"header": "Ustawienia"
}
}

View File

@@ -7,12 +7,12 @@
"edit": "Editar"
},
"buttons": {
"save": "Salvar",
"save": "Guardar",
"logout": "Sair",
"delete": "Excluir",
"edit": "Editar",
"cancel": "Cancelar",
"confirm": "Tem certeza?",
"confirm": "Tem a certeza?",
"filter": "Filtrar",
"clear": "Limpar",
"refresh": "Atualizar",
@@ -22,13 +22,14 @@
"clone": "Clonar",
"archive": "Arquivar",
"unArchive": "Desarquivar",
"hideArchived": "Ocultar arquivados",
"showArchived": "Mostrar arquivado",
"hideArchived": "Ocultar Arquivados",
"showArchived": "Mostrar Arquivado",
"notAccessTitle": "Você não tem permissão para acessar",
"hideColumns": "Ocultar colunas",
"clearFilters": "Limpar filtros",
"hideColumns": "Ocultar Colunas",
"clearFilters": "Limpar Filtros",
"create": "Criar",
"saveAndAdd": "Salvar e adicionar"
"saveAndAdd": "Guardar e Adicionar",
"continue": "Continuar"
},
"warnWhenUnsavedChanges": "Você tem certeza de que quer sair? Você tem alterações não salvas.",
"notifications": {
@@ -41,7 +42,9 @@
"deleteError": "Erro ao excluir {{resource}} (código de status: {{statusCode}})",
"editSuccess": "{{resource}} editado com sucesso",
"editError": "Erro ao editar {{resource}} (código de status: {{statusCode}})",
"importProgress": "Importando: {{processed}}/{{total}}"
"importProgress": "Importando: {{processed}}/{{total}}",
"saveSuccessful": "Salvo com sucesso!",
"validationError": "Erro de validação: {{error}}"
},
"kofi": "Doe no Ko-fi",
"loading": "Carregando",
@@ -52,112 +55,146 @@
"clone": "Clonar"
},
"dashboard": {
"title": "Dashboard"
"title": "Painel"
},
"printing": {
"generic": {
"title": "Imprimindo",
"helpMargin": "As margens devem ser configuradas para corresponder ao papel de etiqueta e à impressora. Alterá-las afetará o tamanho de toda a grade.",
"helpMargin": "As margens devem ser configuradas para corresponder à sua etiqueta de papel e à impressora. Alterá-las afetará o tamanho de toda a grade.",
"helpPrinterMargin": "A zona segura deve ser definida para o quão próximo da borda do papel sua impressora pode imprimir, alterá-la não afetará toda a grade.",
"previewScale": "Visualizar escala",
"skipItems": "Pular itens",
"horizontalSpacing": "Espaçamento horizontal",
"marginRight": "Margem direita",
"marginTop": "Margem superior",
"description": "Ajuste as configurações abaixo para o layout de impressão desejado. Tenha em mente que as impressoras e o seu SO podem aplicar suas próprias margens e dimensionamento, portanto, pode ser necessário realizar algumas tentativas e erros antes que tudo esteja correto. Teste em um pedaço de papel comum antes de imprimir nas etiquetas reais.",
"previewScale": "Visualizar Escala",
"skipItems": "Pular Itens",
"horizontalSpacing": "Espaçamento Horizontal",
"marginRight": "Margem Direita",
"marginTop": "Margem Superior",
"description": "Ajuste as configurações abaixo para obter o layout de impressão desejado. Tenha em mente que impressoras e sistemas operacionais podem aplicar suas próprias margens e escalonamento, portanto, pode ser necessário fazer algumas tentativas antes que tudo estaja correto. Teste em uma folha de papel comum antes de imprimir nas etiquetas reais.",
"print": "Imprimir",
"columns": "Colunas",
"dimensions": "Dimensões",
"contentSettings": "Configurações de conteúdo",
"layoutSettings": "Configurações de layout",
"contentSettings": "Configurações de Conteúdo",
"layoutSettings": "Configurações de Layout",
"rows": "Linhas",
"paperSize": "Tamanho do papel",
"paperSize": "Tamanho do Papel",
"customSize": "Personalizado",
"showBorder": "Mostrar borda",
"verticalSpacing": "Espaçamento vertical",
"marginLeft": "Margem esquerda",
"marginBottom": "Margem inferior",
"printerMarginBottom": "Zona segura inferior",
"printerMarginLeft": "Zona segura esquerda",
"printerMarginRight": "Zona segura direita",
"showBorder": "Mostrar Borda",
"verticalSpacing": "Espaçamento Vertical",
"marginLeft": "Margem Esquerda",
"marginBottom": "Margem Inferior",
"printerMarginBottom": "Área Segura Inferior",
"printerMarginLeft": "Área Segura Esquerda",
"printerMarginRight": "Área Segura Direita",
"borders": {
"none": "Nenhuma",
"border": "Borda",
"grid": "Grade"
},
"printerMarginTop": "Zona segura superior"
"printerMarginTop": "Área Segura Superior",
"settings": "Pré-definições",
"defaultSettings": "Por omissão",
"addSettings": "Adicionar Pré-Definição",
"newSetting": "Novo",
"deleteSettings": "Remover Pré-definição Actual",
"deleteSettingsConfirm": "Tem a certeza que pretender remover esta pré-definição?",
"settingsName": "Nome da Pré-definição",
"itemCopies": "Cópias do Artigo",
"duplicateSettings": "Duplicar",
"saveSetting": "Guardar Pré-definições",
"saveAsImage": "Guardar como Imagem"
},
"spoolSelect": {
"selectedTotal_one": "{{count}} carretel selecionado",
"selectedTotal_many": "{{count}} carretéis selecionados",
"selectedTotal_other": "{{count}} carretéis selecionados",
"noSpoolsSelected": "Você não selecionou nenhum carretel.",
"title": "Selecionar carretéis",
"description": "Selecione carretéis para imprimir códigos QR.",
"showArchived": "Mostrar arquivado",
"selectAll": "Selecionar/desmarcar tudo"
"title": "Selecionar Carretéis",
"description": "Selecione os carretéis para imprimir as etiquetas.",
"showArchived": "Mostrar Arquivado",
"selectAll": "Selecionar/Desmarcar Tudo"
},
"qrcode": {
"title": "Imprimindo códigos QR",
"button": "Imprimir códigos QR",
"spoolWeight": "Peso do carretel: {{weight}}",
"lotNr": "Número do lote: {{lot}}",
"bedTemp": "Temperatura da mesa: {{temp}}",
"extruderTemp": "Temperatura do bico: {{temp}}",
"textSize": "Tamanho do texto do conteúdo",
"title": "Impressão de Etiqueta",
"button": "Imprimir Etiquetas",
"spoolWeight": "Peso do Carretel: {{weight}}",
"lotNr": "Número do Lote: {{lot}}",
"bedTemp": "Temperatura da Mesa: {{temp}}",
"extruderTemp": "Temperatura do Bico: {{temp}}",
"textSize": "Tamanho do Texto da Etiqueta",
"showSpoolmanIcon": "Mostrar ícone do Spoolman",
"showVendor": "Fornecedor",
"showContent": "Etiqueta de impressão",
"showLotNr": "Número do lote",
"showSpoolWeight": "Peso do carretel",
"showVendor": "Fabricante",
"showContent": "Imprimir Etiqueta",
"showLotNr": "Número do Lote",
"showSpoolWeight": "Peso do Carretel",
"showTemperatures": "Temperaturas",
"showSpoolComment": "Comentário do carretel",
"showFilamentComment": "Comentário de filamento",
"showVendorComment": "Comentário do fornecedor"
"showSpoolComment": "Comentário sobre o Carretel",
"showFilamentComment": "Comentário sobre o Filamento",
"showVendorComment": "Comentário sobre o Fabricante",
"template": "Modelo de Etiqueta",
"templateHelp": "Use {} para incluir valores da bobine como texto. Por exemplo {id} será substituído pelo ID da bobine, ou {filament.material} será substituído pelo material da bobine. Coloque texto entre duplos asteriscos ** para colocar a negrito. Pressione o botão para ver a lista de etiquetas disponíveis.",
"showQRCode": "Imprimir Código QR",
"showQRCodeMode": {
"no": "Não",
"simple": "Simples",
"withIcon": "Com Ícone"
}
}
},
"spool": {
"fields": {
"material": "Material",
"weight_to_use": "Peso",
"used_weight": "Peso usado",
"remaining_weight": "Peso restante",
"measured_weight": "Peso medido",
"used_length": "Comprimento usado",
"remaining_length": "Comprimento restante",
"used_weight": "Peso Utilizado",
"remaining_weight": "Peso Restante",
"measured_weight": "Peso Medido",
"used_length": "Comprimento Usado",
"remaining_length": "Comprimento Restante",
"location": "Localização",
"lot_nr": "Número do lote",
"first_used": "Usado pela primeira vez",
"last_used": "Usado por último",
"lot_nr": "Número do Lote",
"first_used": "Primeiro Uso",
"last_used": "Último Uso",
"registered": "Registrado",
"comment": "Comentário",
"archived": "Arquivado",
"id": "ID",
"filament_name": "Filamento",
"filament": "Filamento",
"price": ""
"price": "Preço",
"initial_weight": "Peso Inicial",
"spool_weight": "Peso sem Filamento",
"filament_internal": "Interno",
"filament_external": "Externo"
},
"fields_help": {
"weight_to_use": "Selecione qual valor de peso inserir. O peso medido só estará disponível se o peso do carretel estiver definido para o filamento selecionado.",
"lot_nr": "Número de lote do fabricante. Pode ser usado para garantir que uma impressão tenha cores consistentes se vários carretéis forem usados.",
"used_weight": "Quanto filamento foi usado no carretel. Um novo carretel deve ter 0g usado.",
"used_weight": "Quanto filamento foi usado do carretel. Um novo carretel deve ter 0gr usado.",
"remaining_weight": "Quanto filamento resta no carretel. Para um carretel novo, isso deve corresponder ao peso do carretel.",
"measured_weight": "Quanto pesa o filamento e o carretel.",
"location": "Onde o carretel está localizado se você tiver vários locais onde armazena seus carretéis.",
"price": ""
"price": "Preço de um carretel completo na moeda definida no sistema. Se não for definido, o preço do filamento será presumido.",
"initial_weight": "O peso inicial do filamento sem o peso do carretel (peso liquido). Se nada for indicado, será usado o peso da definição do filamento.",
"spool_weight": "O peso do carretel quando vazio. Deixe em branco para usar o valor do filamento ou do fabricante.",
"external_filament": "Escolheu um filamento de uma base de dados externa. O filamento (e possivelmente o fabricante) será criado automaticamente quando for criado o carretel. Isto poderá criar filamentos duplicados caso já tenha criado um filamento para este filamento específico."
},
"titles": {
"create": "Criar carretel",
"clone": "Clonar carretel",
"edit": "Editar carretel",
"create": "Criar Carretel",
"clone": "Clonar Carretel",
"edit": "Editar Carretel",
"list": "Carretéis",
"show": "Mostrar carretel",
"show": "Mostrar Carretel",
"show_title": "[Carretel #{{id}}] {{name}}",
"archive": "Arquivar carretel"
"archive": "Arquivar Carretel",
"adjust": "Ajustar Filamento"
},
"form": {
"new_location_prompt": "Insira um novo local",
"spool_updated": "Este spool foi atualizado por alguém/alguma coisa desde que você abriu esta página. Salvar substituirá essas alterações!"
"spool_updated": "Este carretel foi atualizado por alguém desde que abriu esta página. Guardar substituirá essas alterações!",
"measurement_type": {
"length": "Comprimento",
"weight": "Peso"
},
"measurement_type_label": "Tipo de Medida",
"adjust_filament_help": "Aqui pode adicionar ou subtrair filamento do carretel. Um valor positivo consome filamento, um valor negativo adiciona filamento.",
"adjust_filament_value": "Consumir Montante"
},
"messages": {
"archive": "Tem certeza de que deseja arquivar este carretel?"
@@ -168,65 +205,81 @@
"filament": "Filamentos",
"fields": {
"id": "ID",
"vendor_name": "Fornecedor",
"vendor_name": "Fabricante",
"name": "Nome",
"price": "Preço",
"diameter": "Diâmetro",
"weight": "Peso",
"spool_weight": "Peso do carretel",
"article_number": "Número do artigo",
"spool_weight": "Peso do Carretel",
"article_number": "Número do Artigo",
"registered": "Registrado",
"vendor": "Fornecedor",
"vendor": "Fabricante",
"material": "Material",
"density": "Densidade",
"comment": "Comentário",
"settings_extruder_temp": "Temperatura do bico",
"settings_bed_temp": "Temperatura da mesa",
"settings_extruder_temp": "Temperatura do Bico",
"settings_bed_temp": "Temperatura da Mesa",
"color_hex": "Cor",
"spools": "Mostrar carretéis"
"spools": "Mostrar Carretéis",
"external_id": "ID Externo",
"single_color": "Único",
"multi_color": "Múltiplo",
"coaxial": "Coextrusado",
"longitudinal": "Longitudinal"
},
"fields_help": {
"article_number": "Por exemplo EAN, UPC, etc.",
"price": "Preço de um spool completo na moeda configurada pelo sistema.",
"name": "Nome do filamento, para distinguir este tipo de filamento entre outros do mesmo fornecedor. Deve conter a cor, por exemplo.",
"price": "Preço de um carretel completo.",
"name": "Nome do filamento, para distinguir este tipo de filamento entre outros do mesmo fabricante. Deve conter a cor, por exemplo.",
"material": "Por exemplo PLA, ABS, PETG, etc.",
"weight": "O peso do filamento de uma carretel cheio (peso líquido). Isso não deve incluir o peso do carretel em si, apenas o filamento. É o que normalmente está escrito na embalagem.",
"spool_weight": "O peso de um carretel vazio. Usado para determinar o peso medido de um carretel."
"weight": "O peso do filamento de um carretel cheio (peso líquido). Isso não deve incluir o peso do carretel em si, apenas o filamento. É o que normalmente está escrito na embalagem.",
"spool_weight": "O peso de um carretel vazio. Usado para determinar o peso medido de um carretel.",
"multi_color_direction": "Os filamentos podem ter múltiplas cores de duas formas: por coextrusão, como os filamentos de duas cores com estas consistentes ao longo do filamento, or através de mudança de cor longitudinal, como os filamentos que contêm gradientes de cores que vão mudando ao longo da bobine."
},
"titles": {
"create": "Criar filamento",
"clone": "Clonar filamento",
"edit": "Editar filamento",
"create": "Criar Filamento",
"clone": "Clonar Filamento",
"edit": "Editar Filamento",
"list": "Filamentos",
"show": "Mostrar filamento",
"show": "Mostrar Filamento",
"show_title": "[Filamento #{{id}}] {{name}}"
},
"form": {
"filament_updated": "Este filamento foi atualizado por alguém/alguma coisa desde que você abriu esta página. Salvar substituirá essas alterações!"
"filament_updated": "Este filamento foi atualizado por alguém desde que abriu esta página. Guardar substituirá essas alterações!",
"import_external": "Importar do Exterior",
"import_external_description": "Escolha um filamento da lista para preencher automaticamente os detalhes do filamento no formulário de criação. Isto irá substituir quaisquer dados que já tenha introduzido no formulário.<br><br>Será criado o fabricante automaticamente caso seja necessário."
},
"buttons": {
"add_spool": "Adicionar Carretel"
}
},
"vendor": {
"vendor": "Fornecedores",
"vendor": "Fabricantes",
"fields": {
"id": "ID",
"name": "Nome",
"registered": "Registrado",
"comment": "Comentário"
"comment": "Comentário",
"empty_spool_weight": "Peso do Carretel Vazio",
"external_id": "ID Externo"
},
"titles": {
"create": "Criar fornecedor",
"clone": "Clonar fornecedor",
"edit": "Editar fornecedor",
"list": "Fornecedores",
"show": "Mostrar fornecedor",
"show_title": "[Fornecedor nº{{id}}] {{name}}"
"create": "Criar Fabricante",
"clone": "Clonar Fabricante",
"edit": "Editar Fabricante",
"list": "Fabricantes",
"show": "Mostrar Fabricante",
"show_title": "[Fabricante #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "Este fornecedor foi atualizado por alguém/alguma coisa desde que você abriu esta página. Salvar substituirá essas alterações!"
"vendor_updated": "Este fabricante foi atualizado por alguém desde que abriu esta página. Guardar substituirá essas alterações!"
},
"fields_help": {
"empty_spool_weight": "O peso do carretel vazio do fabricante."
}
},
"home": {
"home": "Home"
"home": "Início"
},
"help": {
"help": "Ajuda",
@@ -235,12 +288,12 @@
"spool": "Carretéis físicos individuais de um filamento específico.",
"vendor": "As empresas que fabricam o filamento."
},
"description": "<title>Ajuda</title><p>Aqui estão algumas dicas para você começar.</p><p>O Spoolman contém três tipos diferentes de dados:</p><itemsHelp/><p>Para adicionar um novo carretel para o banco de dados, comece criando um objeto <filamentCreateLink>Filamento</filamentCreateLink>. Feito isso, prossiga para criar um objeto <spoolCreateLink>Carretel </spoolCreateLink> para esse carretel específico. Se você adquirir carretéis adicionais do mesmo filamento posteriormente, simplesmente gere objetos Spool adicionais e reutilize o mesmo objeto filamento.</p><p>Opcionalmente, você também pode gerar um objeto <vendorCreateLink>Fornecedor</vendorCreateLink> para a empresa fabricar o filamento se desejar rastrear essas informações.</p><p>Você tem a opção de vincular outros serviços de impressora 3D ao Spoolman, como o Moonraker, que pode monitorar automaticamente o uso do filamento e atualizar os objetos carretel para você. Consulte o <readmeLink>README do Spoolman</readmeLink> para obter orientação sobre como configurar isso.</p>"
"description": "<title>Ajuda</title><p>Aqui estão algumas dicas para começar.</p><p>O Spoolman contém três tipos diferentes de dados:</p><itemsHelp/><p>Para adicionar um novo carretel para o banco de dados, comece criando um <filamentCreateLink>Filamento</filamentCreateLink>. Feito isso, prossiga para criar um <spoolCreateLink>Carretel</spoolCreateLink> para esse carretel específico. Se você adquirir carretéis adicionais do mesmo filamento posteriormente, simplesmente gere Carretéis adicionais e reutilize o mesmo objeto Filamento.</p><p>Opcionalmente, você também pode criar um <vendorCreateLink>Fabricante</vendorCreateLink> para a empresa que fabrica o filamento, se desejar rastrear essas informações.</p><p>Você tem a opção de vincular outros serviços de impressora 3D ao Spoolman, como o Moonraker, que pode monitorar automaticamente o uso do filamento e atualizar os carretéis para você. Consulte o <readmeLink>README do Spoolman</readmeLink> para obter orientação sobre como configurar isso.</p>"
},
"documentTitle": {
"suffix": " | Spoolman",
"home": {
"list": "Home | Spoolman"
"list": "Início | Spoolman"
},
"help": {
"list": "Ajuda | Spoolman"
@@ -248,40 +301,81 @@
"filament": {
"list": "Filamentos | Spoolman",
"show": "#{{id}} Mostrar filamento | Spoolman",
"edit": "#{{id}} Editar filamento | Spoolman",
"edit": "#{{id}} Editar Filamento | Spoolman",
"create": "Criar filamento | Spoolman",
"clone": "#{{id}} Clonar filamento | Spoolman"
},
"spool": {
"list": "Carretéis | Spoolman",
"show": "#{{id}} Mostrar carretel | Spoolman",
"edit": "#{{id}} Editar carretel | Spoolman",
"create": "Criar carretel | Spoolman",
"clone": "#{{id}} Clonar carretel | Spoolman"
"show": "#{{id}} Mostrar Carretel | Spoolman",
"edit": "#{{id}} Editar Carretel | Spoolman",
"create": "Criar Carretel | Spoolman",
"clone": "#{{id}} Clonar Carretel | Spoolman"
},
"vendor": {
"show": "#{{id}} Mostrar fornecedor | Spoolman",
"list": "Fornecedores | Spoolman",
"create": "Criar fornecedor | Spoolman",
"edit": "#{{id}} Editar fornecedor | Spoolman",
"clone": "#{{id}} Clonar fornecedor | Spoolman"
"show": "#{{id}} Mostrar Fabricante | Spoolman",
"list": "Fabricantes | Spoolman",
"create": "Criar Fabricante | Spoolman",
"edit": "#{{id}} Editar Fabricante | Spoolman",
"clone": "#{{id}} Clonar Fabricante | Spoolman"
},
"default": "Spoolman"
},
"no": "Não",
"scanner": {
"title": "Leitor de código QR",
"title": "Leitor de Código QR",
"description": "Digitalize um código QR do Spoolman para ver detalhes sobre o carretel.",
"error": {
"notAllowed": "Você não permitiu acesso à câmera.",
"insecureContext": "A página não é veiculada por HTTPS.",
"notAllowed": "Você não deu permissão de acesso à câmera.",
"insecureContext": "A página não é protegida por HTTPS.",
"streamApiNotSupported": "O navegador não oferece suporte à API MediaStream.",
"unknown": "Ocorreu um erro desconhecido. ({{error}})",
"notReadable": "A câmera não é legível.",
"notReadable": "A câmera não está acessível.",
"notFound": "Nenhuma câmera foi encontrada."
}
},
"table": {
"actions": "Ações"
},
"settings": {
"extra_fields": {
"params": {
"multi_choice": "Múltiplas Opções",
"key": "Chave",
"name": "Nome",
"field_type": "Tipo",
"unit": "Unidade",
"order": "Ordem",
"default_value": "Valor Padrão",
"choices": "Opções"
},
"field_type": {
"datetime": "Date e Hora",
"text": "Texto",
"integer": "Inteiro",
"float": "Decimal",
"float_range": "Intervalo Decimal",
"integer_range": "Intervalo Inteiro",
"boolean": "Booleano",
"choice": "Opção"
},
"key_not_changed": "Por favor, mude a chave por outra.",
"delete_confirm": "Apagar o campo {{name}}?",
"delete_confirm_description": "Isto irá apagar o campo e todos os dados associados em todas as entidades.",
"description": "<p>Aqui você pode adicionar campos personalizados extras às suas entidades.</p><p>Uma vez que um campo é adicionado, você não pode alterar sua chave ou tipo, e para campos do tipo \"opção\" você não pode remover as opções ou alterar o estado de múltipla opção. Se você remover um campo, os dados associados para todas as entidades serão excluídos.</p><p>A chave é como outros programas leem/gravam os dados, portanto, se seu campo personalizado se destina a se integrar com um programa de terceiros, certifique-se de defini-lo corretamente. O valor padrão só é aplicado a novos itens.</p><p>Campos extras não podem ser ordenados ou filtrados nas visualizações de tabela.</p>",
"choices_missing_error": "Você não pode remover as opções. As seguintes opções serão perdidas: {{choices}}",
"tab": "Campos Extras",
"boolean_true": "Sim",
"boolean_false": "Não",
"non_unique_key_error": "A chave deve ser única."
},
"settings": "Configurações",
"header": "Configurações",
"general": {
"tab": "Geral",
"currency": {
"label": "Moeda"
}
}
}
}

View File

@@ -30,18 +30,20 @@
"show": "arata",
"edit": "editeaza"
},
"warnWhenUnsavedChanges": "Esti sigur? Modificarile ne-salvate vor fi pierdurte",
"warnWhenUnsavedChanges": "Esti sigur? Modificarile ne-salvate vor fi pierdute.",
"notifications": {
"success": "succes",
"error": "eroare",
"undoable": "Ai inca {{seconds}} secunde sa corectezi",
"error": "Eroare (Cod: {{statusCode}})",
"undoable": "Ai inca {{seconds}} secunde sa corectezi.",
"createSuccess": "{{resource}} creata cu succes",
"createError": "Eroare la crearea {{resource}} (status code: {{statusCode}})",
"createError": "Eroare la crearea {{resource}} (status code: {{statusCode}})",
"deleteSuccess": "{{resource}} stearsa cu succes",
"deleteError": "Eroare la stergerea {{resource}} (status code: {{statusCode}})",
"deleteError": "Eroare la stergerea {{resource}} (status code: {{statusCode}})",
"editSuccess": "{{resource}} editata cu succes",
"editError": "eroare la editarea {{resource}} (status code: {{statusCode}})",
"importProgress": "Importare: {{processed}}/{{total}}"
"editError": "eroare la editarea {{resource}} (status code: {{statusCode}})",
"importProgress": "Importare: {{processed}}/{{total}}",
"saveSuccessful": "Salvare reusita!",
"validationError": "Eroare de validare: {{error}}"
},
"kofi": "Doneaza pe Ko-fi",
"loading": "Incarca",
@@ -62,56 +64,56 @@
},
"previewScale": "Scala de previzualizare",
"contentSettings": "Setări de conținut",
"helpMargin": "",
"description": "",
"helpMargin": "Marginile trebuie configurate pentru a se potrivi cu hârtia de etichetă și imprimanta dvs., modificarea acestora va afecta dimensiunea întregii grile.",
"description": "Modificati setările de mai jos pentru a obține aspectul dorit al etichetelor. Rețineți că imprimantele și sistemul dvs. de operare pot aplica propriile setări, așa că poate fi necesar să incercati mai multe opțiuni înainte ca totul să fie corect. Testați-l pe o bucată de hârtie obișnuită înainte de a imprima pe etichetele reale.",
"marginRight": "Marja dreaptă",
"rows": "Rânduri",
"marginBottom": "Marja de jos",
"marginTop": "Marja superioară",
"print": "Imprimare",
"printerMarginRight": "",
"printerMarginRight": "Safe-Zone dreapta",
"columns": "Coloane",
"helpPrinterMargin": "",
"helpPrinterMargin": "Safe-Zone ar trebui să fie setată la cât de aproape de marginea hârtiei poate imprima imprimanta dvs., modificarea acestora nu va afecta întreaga grilă.",
"layoutSettings": "Setări Layout",
"dimensions": "Dimensiuni",
"skipItems": "Treci peste articole",
"paperSize": "Dimensiunea hârtiei",
"printerMarginTop": "",
"printerMarginTop": "Safe-Zone sus",
"horizontalSpacing": "Spațiere orizontală",
"customSize": "Personalizat",
"marginLeft": "Margine stângă",
"printerMarginLeft": "",
"printerMarginLeft": "Safe-Zone stanga",
"showBorder": "Arată marginea",
"printerMarginBottom": "",
"printerMarginBottom": "Safe-Zone jos",
"title": "Imprimare"
},
"spoolSelect": {
"showArchived": "",
"showArchived": "Arata arhivate",
"title": "Selectați bobine",
"description": "",
"selectedTotal_one": "",
"selectedTotal_few": "",
"selectedTotal_other": "",
"selectAll": "",
"noSpoolsSelected": ""
"description": "Selectati bobinele pentru care trebuie printate etichete.",
"selectedTotal_one": "{{count}} bobina selectata",
"selectedTotal_few": "{{count}} bobine selectate",
"selectedTotal_other": "{{count}} bobine selectate",
"selectAll": "Selecteaza/deselecteaza tot",
"noSpoolsSelected": "Nu ati selectat nicio bobina."
},
"qrcode": {
"bedTemp": "",
"title": "",
"button": "",
"bedTemp": "BT: {{temp}}",
"title": "Printati codurile QR",
"button": "Imprimati codurile QR",
"showTemperatures": "Temperaturi",
"spoolWeight": "",
"spoolWeight": "Greutatea rolei de filament: {{weight}}",
"showFilamentComment": "Filament Comentariu",
"showLotNr": "",
"showLotNr": "Lot Nr",
"showContent": "Imprimă eticheta",
"showSpoolWeight": "Greutate bobină",
"textSize": "",
"textSize": "Marimea textului",
"showVendorComment": "Comentariul furnizorului",
"showSpoolmanIcon": "",
"showSpoolmanIcon": "Arata iconul Spoolman",
"showSpoolComment": "Bobină Comentariu",
"extruderTemp": "",
"extruderTemp": "ET: {{temp}}",
"showVendor": "Furnizor",
"lotNr": ""
"lotNr": "Lot Nr: {{lot}}"
}
},
"documentTitle": {
@@ -158,54 +160,54 @@
"fields": {
"weight_to_use": "Greutate",
"registered": "Înregistrat",
"used_weight": "",
"first_used": "",
"used_weight": "Greutate folosita",
"first_used": "Prima utilizare",
"location": "Locație",
"material": "Material",
"last_used": "",
"last_used": "Ultima utilizare",
"archived": "Arhivat",
"price": "Preț",
"used_length": "",
"measured_weight": "",
"lot_nr": "",
"used_length": "Lungime folosita",
"measured_weight": "Greutate masurata",
"lot_nr": "Lot Nr",
"comment": "Comentariu",
"id": "ID",
"filament": "Filament",
"filament_name": "Filament",
"remaining_length": "",
"remaining_weight": ""
"remaining_length": "Lungime ramasa",
"remaining_weight": "Greutate ramasa"
},
"fields_help": {
"location": "",
"weight_to_use": "",
"used_weight": "",
"price": "",
"lot_nr": "",
"remaining_weight": "",
"measured_weight": ""
"location": "Locatia in care bobina este stocata.",
"weight_to_use": "Selectati greutatea care trebuie folosita. Greutatea masurata e disponibila numai daca greutatea bobinei goale este setata pentru filamentul respectiv.",
"used_weight": "Cat filament a fost folosit de pe bobina respectiva. O bobina noua ar trebui sa aiba 0 grame folosite.",
"price": "Pretul unei bobine. Daca nu e definit, pretul filamentului va fi folosit.",
"lot_nr": "Numarul de productie al lotului. Poate fi folosit pentru a garanta consistenta culorii, in caz ca mai multe bobine sunt folosite la acelasi print.",
"remaining_weight": "Cat filament a ramas pe bobina. Aceasta valoare reprezinta greutatea filamentului de pe bobina respectiva.",
"measured_weight": "Greutatea totala a bobinei si filamentului."
},
"titles": {
"list": "Bobine",
"archive": "",
"show_title": "",
"archive": "Archiveaza bobina",
"show_title": "[Bobina #{{id}}] {{name}}",
"create": "Creați bobină",
"show": "",
"edit": "",
"clone": ""
"show": "Arata bobina",
"edit": "Editeaza bobina",
"clone": "Cloneaza bobina"
},
"messages": {
"archive": ""
"archive": "Esti sigur ca vrei sa arhivezi aceasta bobina?"
},
"form": {
"spool_updated": "",
"new_location_prompt": ""
"spool_updated": "Aceasta bobina a fost modificata de altcineva in timpul scurs de la deschiderea acestei pagini, Salvarea va suprascrie aceste schimbari!",
"new_location_prompt": "Introduceti o locatie noua"
},
"spool": "Bobine"
},
"filament": {
"fields": {
"registered": "Înregistrat",
"article_number": "",
"article_number": "Numarul articolului",
"id": "ID",
"vendor": "Furnizor",
"settings_bed_temp": "Temperatura patului",
@@ -213,22 +215,22 @@
"price": "Preț",
"name": "Nume",
"color_hex": "Culoare",
"settings_extruder_temp": "",
"settings_extruder_temp": "Temperatura extruderului",
"vendor_name": "Furnizor",
"diameter": "Diametru",
"density": "Densitate",
"weight": "Greutate",
"spools": "",
"spools": "Arata bobinele",
"comment": "Comentariu",
"spool_weight": ""
"spool_weight": "Greutatea bobinei"
},
"fields_help": {
"price": "",
"material": "",
"name": "",
"spool_weight": "",
"price": "Pretului unei bobine noi.",
"material": "Ex. PLA, ABS, PETG, etc.",
"name": "Numele filamentului, pentru a-l putea distinge de altele ale aceluiasi producator. Recomandabil sa contina culoarea de exemplu.",
"spool_weight": "Greutatea unei bobine goale (fara filament). Este folosita pentru a calcula greutatea unei bobine cu filament.",
"article_number": "",
"weight": ""
"weight": "Greutatea neta a filamentului de pe o bobina. Nu trebuie sa includa greutatea bobinei, ci doar cea a filamentului. De obicei greutatea filementului este cea inscrisa pe ambalaj."
},
"titles": {
"edit": "Modifica del filamento",
@@ -271,15 +273,15 @@
},
"scanner": {
"error": {
"notFound": "",
"unknown": "",
"notAllowed": "",
"streamApiNotSupported": "",
"insecureContext": "",
"notReadable": ""
"notFound": "Nicio camera nu a fost gasita.",
"unknown": "Eroare necunoscuta. ({{error}})",
"notAllowed": "Accesul la camera nu a fost permis.",
"streamApiNotSupported": "Browserul nu suporta MediaStream API.",
"insecureContext": "Aceasta pagina nu este servita peste HTTPS.",
"notReadable": "Accesul la camera nu este posibil."
},
"title": "",
"description": ""
"title": "Scanner QR code",
"description": "Scanati QR code-ul Spoolman pentru detalii."
},
"table": {
"actions": "Acțiuni"

View File

@@ -87,7 +87,15 @@
"none": "Без рамки",
"border": "Общая",
"grid": "Сетка"
}
},
"addSettings": "Добавить новую настройку",
"newSetting": "Новый",
"deleteSettings": "Удалить текущую настройку",
"deleteSettingsConfirm": "Вы уверены, что хотите удалить этот параметр?",
"settings": "Настройки",
"defaultSettings": "По умолчанию",
"settingsName": "Имя настройки",
"itemCopies": "Копии элементов"
},
"qrcode": {
"button": "Распечатать QR-коды",
@@ -105,7 +113,9 @@
"showTemperatures": "Температура",
"showSpoolComment": "Комментарий к катушке",
"showFilamentComment": "Комментарий к филаменту",
"showVendorComment": "Комментарий к продавцу"
"showVendorComment": "Комментарий к продавцу",
"template": "Шаблон",
"templateHelp": "Используйте {}, чтобы вставить значения объекта катушки в виде текста. Например, {id} будет заменен на идентификатор катушки, или {filament.material} будет заменен на материал катушки. Выделите текст двойной звездочкой **, чтобы он был выделен жирным шрифтом. Нажмите на кнопку, чтобы просмотреть список всех доступных тегов."
},
"spoolSelect": {
"title": "Выберите катушки",
@@ -150,7 +160,11 @@
"archived": "В архиве",
"measured_weight": "Измеренный вес",
"weight_to_use": "Масса",
"price": "Цена"
"price": "Цена",
"initial_weight": "Начальный вес",
"spool_weight": "Пустой вес",
"filament_internal": "Внутренний",
"filament_external": "Внешний"
},
"fields_help": {
"used_weight": "Сколько филамента было использовано с катушки. На новой катушке должно быть использовано 0 г.",
@@ -159,7 +173,10 @@
"weight_to_use": "Выберите, какое значение веса ввести. Измеренный вес доступен только в том случае, если вес катушки установлен для выбранной филамента.",
"measured_weight": "Сколько весит филамент и катушка.",
"remaining_weight": "Сколько филамента осталось на катушке. Для новой катушки этот вес должен соответствовать весу катушки.",
"price": "Цена полной катушки в валюте, настроенной системой. Если не задано, вместо этого будет принята цена филомента."
"price": "Цена полной катушки. Если не указано, будет использоваться цена филамента.",
"initial_weight": "Исходный вес филамента на катушке (вес нетто). Если не задан, будет использоваться вес объекта, на котором находится филамент.",
"spool_weight": "Вес пустой катушки. Оставьте значение \"пусто\", чтобы использовать значение, указанное в описании филамента или у производителя.",
"external_filament": "Вы выбрали филамент из внешней базы данных. Объект филамент (и, возможно, объект производителя) будет создан автоматически при создании этой катушки. При этом могут возникнуть повторяющиеся объекты филамента, если вы уже создали объект филамент для этой нити накала."
},
"titles": {
"create": "Создать Катушку",
@@ -197,15 +214,21 @@
"settings_extruder_temp": "Температура экструдера",
"settings_bed_temp": "Температура стола",
"color_hex": "Цвет",
"spools": "Показать катушки"
"spools": "Показать катушки",
"coaxial": "Коэкструдированный",
"longitudinal": "Продольный",
"single_color": "Одноцветный",
"multi_color": "Многоцветный",
"external_id": "Внешний ID"
},
"fields_help": {
"name": "Название филамента, чтобы отличить этот тип филамента от друго филамента того же производителя. Например, должен содержать цвет.",
"name": "Название филамента, чтобы отличить этот тип филамента от других того же производителя. Например, должно содержать цвет.",
"material": "Например: PLA, ABS, PETG и т.д.",
"price": "Цена полной катушки в валюте, настроенной системой.",
"price": "Цена полной катушки.",
"weight": "Вес филамента на полной катушке (вес нетто). Сюда не следует включать вес самой катушки, а только филамента. Это то, что обычно пишут на упаковке.",
"spool_weight": "Вес пустой катушки. Используется для определения измеренного веса катушки.",
"article_number": "Например: EAN, UPC, и т.д."
"article_number": "Например: EAN, UPC, и т.д.",
"multi_color_direction": "Филаменты могут иметь несколько цветов двумя способами: либо через коэкструзию, как двойные цветные филаменты с постоянными разноцветными участками, либо через продольные изменения цвета, как градиентные филаменты, которые меняют цвет по всей длине катушки."
},
"titles": {
"create": "Создать филамент",
@@ -216,7 +239,9 @@
"show_title": "[Филамент #{{id}}] {{name}}"
},
"form": {
"filament_updated": "Этот филамент был обновлен кем-то/чем-то другим с тех пор, как вы открыли эту страницу. При сохранении эти изменения перезапишутся!"
"filament_updated": "Этот филамент был обновлен кем-то/чем-то другим с тех пор, как вы открыли эту страницу. При сохранении эти изменения перезапишутся!",
"import_external": "Импорт из внешнего источника",
"import_external_description": "Выберите филамент из списка, чтобы автоматически заполнить его данные в форме создания филамента. Это перезапишет все данные, которые вы ввели в форму.<br><br>Объект \"Производитель\" будет создан автоматически, если это необходимо, когда вы нажмете \"Ок\"."
},
"buttons": {
"add_spool": "Добавить катушку"
@@ -228,7 +253,9 @@
"id": "ID",
"name": "Имя",
"registered": "Зарегистрирован",
"comment": "Комментарий"
"comment": "Комментарий",
"empty_spool_weight": "Вес пустой катушки",
"external_id": "Внешний ID"
},
"titles": {
"create": "Создать производителя",
@@ -240,6 +267,9 @@
},
"form": {
"vendor_updated": "Этот производитель был обновлен кем-то/чем-то другим с тех пор, как вы открыли эту страницу. При сохранении эти изменения перезапишутся!"
},
"fields_help": {
"empty_spool_weight": "Вес пустой катушки от этого производителя."
}
},
"table": {
@@ -282,7 +312,7 @@
"filament": "Марка филамента. У них есть такие свойства, как имя, материал, цвет, диаметр и многое другое.",
"vendor": "Компании, производящие филаменты."
},
"description": "<title>Помощь</title><p>Вот несколько советов, которые помогут вам начать</p><p>Spoolman хранит 3 различных типа данных:</p><itemsHelp/><p>Чтобы добавить новую катушку в базу данных, начните с создания объекта <filamentCreateLink>Филамент</filamentCreateLink>. Как только это будет сделано, приступайте к созданию объекта <spoolCreateLink>Катушка</spoolCreateLink> для этой конкретной катушки. Если позже вы приобретете дополнительные катушки того же филамента, просто создайте дополнительные объекты катушек и повторно используйте тот же объект Филамент.</p><p>При желании вы также можете создать объект <vendorCreateLink>Поставщика</vendorCreateLink> для компании, производящий филамент, если хотите отслеживать эту информацию.</p><p>У вас есть возможность связать Spoolman с другими службами 3D-принтеров, например Moonraker, который может автоматически отслеживать использование филамента и обновлять объекты Катушек для вас. Обратитесь к <readmeLink>Spoolman README</readmeLink> для получения инструкций по настройке.</p>",
"description": "<title>Помощь</title><p>Вот несколько советов, которые помогут вам начать</p><p>Spoolman хранит 3 различных типа данных:</p><itemsHelp/><p>Чтобы добавить новую катушку в базу данных, начните с создания объекта <filamentCreateLink>Филамент</filamentCreateLink>. Как только это будет сделано, приступайте к созданию объекта <spoolCreateLink>Катушка</spoolCreateLink> для этой конкретной катушки. Если позже вы приобретете дополнительные катушки того же филамента, просто создайте дополнительные объекты катушек и повторно используйте тот же объект Филамент.</p><p>При желании вы также можете создать объект <vendorCreateLink>Производитель</vendorCreateLink> для компании, производящий филамент, если хотите отслеживать эту информацию.</p><p>У вас есть возможность связать Spoolman с другими службами 3D-принтеров, например Moonraker, который может автоматически отслеживать использование филамента и обновлять объекты Катушек для вас. Обратитесь к <readmeLink>Spoolman README</readmeLink> для получения инструкций по настройке.</p>",
"help": "Помощь"
},
"kofi": "Поддержите меня на Ko-fi",
@@ -327,6 +357,7 @@
"currency": {
"label": "Валюта"
}
}
},
"settings": "Настройки"
}
}

View File

@@ -28,7 +28,8 @@
"notAccessTitle": "Du har inte tillgång till denna sida",
"hideColumns": "Dölj kolumner",
"clearFilters": "Rensa filter",
"saveAndAdd": "Spara och Lägg till"
"saveAndAdd": "Spara och Lägg till",
"continue": "Fortsätt"
},
"warnWhenUnsavedChanges": "Är du säker på att du vill lämna sidan? Du har osparade ändringar.",
"notifications": {
@@ -87,29 +88,48 @@
"none": "Ingen",
"border": "Ram",
"grid": "Rutnät"
}
},
"defaultSettings": "Standard",
"newSetting": "Ny",
"settingsName": "Inställningsnamn",
"deleteSettingsConfirm": "Är du säker på att du vill radera den nuvarande inställningen?",
"itemCopies": "Kopior",
"settings": "Inställningar",
"addSettings": "Skapa inställning",
"saveSetting": "Spara inställningar",
"duplicateSettings": "Duplicera",
"deleteSettings": "Radera nuvarande inställning",
"saveAsImage": "Spara som bild"
},
"qrcode": {
"button": "Skriv ut QR-koder",
"title": "QR-kod utskrift",
"button": "Skriv ut etiketter",
"title": "Etikettutskrift",
"spoolWeight": "Spolvikt: {{weight}}",
"lotNr": "Batch: {{lot}}",
"bedTemp": "BT: {{temp}}",
"extruderTemp": "ET: {{temp}}",
"textSize": "Textstorlek",
"showSpoolmanIcon": "Visa Spoolman-ikon",
"showContent": "Etikett",
"showVendor": "Leverantör",
"showContent": "Visa text",
"showVendor": "Tillverkare",
"showLotNr": "Batchnummer",
"showSpoolWeight": "Spolvikt",
"showTemperatures": "Temperaturer",
"showSpoolComment": "Spolkommentar",
"showFilamentComment": "Filamentkommentar",
"showVendorComment": "Leverantörskommentar"
"showVendorComment": "Tillverkarkommentar",
"template": "Textmall",
"templateHelp": "Använd {} för att infoga värden från spolobjektet som text. Till exempel kommer {id} att ersättas med spolens id, eller {filament.material} att ersättas med filamentets material. Omslut text med dubbla asterisker ** för att göra den fetstilad. Klicka på knappen för att visa en lista över alla tillgängliga taggar.",
"showQRCode": "Visa QR-kod",
"showQRCodeMode": {
"no": "Nej",
"simple": "Enkel",
"withIcon": "Med logga"
}
},
"spoolSelect": {
"title": "Välj spolar",
"description": "Välj spolar att skriva ut QR-koder för.",
"description": "Välj spolar att skriva ut etiketter för.",
"showArchived": "Tillåt arkiverade",
"noSpoolsSelected": "Du har inte valt några spolar.",
"selectAll": "Markera/avmarkera alla",
@@ -149,7 +169,11 @@
"archived": "Arkiverad",
"measured_weight": "Uppmätt vikt",
"weight_to_use": "Vikt",
"price": "Pris"
"price": "Pris",
"initial_weight": "Initial Vikt",
"spool_weight": "Tom Vikt",
"filament_internal": "Interna",
"filament_external": "Externa"
},
"fields_help": {
"used_weight": "Hur mycket filament som använts på spolen. En ny spole har värdet 0g.",
@@ -158,7 +182,10 @@
"weight_to_use": "Välj vilken typ av vikt du vill fylla i. Uppmätt vikt är endast tillgänglig om det valda filamentet har en spolvikt satt.",
"measured_weight": "Hur mycket filamentet och spolen väger tillsammans.",
"remaining_weight": "Hur mycket filament som finns kvar på spolen. För en ny spole så är detta det värdet som står på etiketten.",
"price": "Pris för en hel rulle i det konfigurerade valutasystemet. Om det inte är inställt kommer priset för filamentet istället att antas."
"price": "Pris för en hel rulle. Om det inte är inställt kommer priset för filamentet istället att antas.",
"initial_weight": "Ursprungsvikt av filament på spolen (nettovikt). Om du inte sätter detta så tas vikten från filamentobjektet.",
"spool_weight": "Vikten på en tom spole. Lämna tom för att ta värdet från filament- eller tillverkarobjektet.",
"external_filament": "Du har valt ett filament från den externa databasen. Ett filamentobjekt (och eventuellt ett tillverkarobjekt) kommer att skapas automatiskt när du skapar denna spole. Detta kan skapa duplicerade filamentobjekt om du redan har skapat ett filamentobjekt för denna filament."
},
"titles": {
"create": "Skapa spole",
@@ -167,22 +194,30 @@
"list": "Spolar",
"show": "Visa spole",
"archive": "Arkivera spole",
"show_title": "[Spole #{{id}}] {{name}}"
"show_title": "[Spole #{{id}}] {{name}}",
"adjust": "Justera Spolmängd"
},
"messages": {
"archive": "Är du säker på att du vill arkivera denna spole?"
},
"form": {
"new_location_prompt": "Fyll i en ny plats",
"spool_updated": "Den här spolen har uppdaterats av något eller någon sedan du öppnade den här sidan. Om du sparar så skriver du över ändringarna!"
"spool_updated": "Den här spolen har uppdaterats av något eller någon sedan du öppnade den här sidan. Om du sparar så skriver du över ändringarna!",
"measurement_type": {
"length": "Längd",
"weight": "Vikt"
},
"measurement_type_label": "Mättyp",
"adjust_filament_value": "Mängd att konsumera",
"adjust_filament_help": "Här kan du direkt lägga till eller subtrahera filament från spolen. Ett positivt värde kommer att konsumera filament, ett negativt värde kommer att lägga till det."
}
},
"filament": {
"filament": "Filament",
"fields": {
"id": "ID",
"vendor_name": "Leverantör",
"vendor": "Leverantör",
"vendor_name": "Tillverkare",
"vendor": "Tillverkare",
"name": "Namn",
"material": "Material",
"price": "Pris",
@@ -196,15 +231,21 @@
"settings_extruder_temp": "Extrudertemp",
"settings_bed_temp": "Bäddtemp",
"color_hex": "Färg",
"spools": "Visa spolar"
"spools": "Visa spolar",
"single_color": "Enkel",
"multi_color": "Flerfärgad",
"coaxial": "Coextruderad",
"longitudinal": "Longitudinell",
"external_id": "Externt ID"
},
"fields_help": {
"name": "Filamentets namn, för att skilja detta filament från andra från samma leverantör. Bör innehålla färgen till exempel.",
"name": "Filamentets namn, för att skilja detta filament från andra från samma tillverkare. Bör innehålla färgen till exempel.",
"material": "T.ex. PLA, ABS, PETG, etc.",
"price": "Pris för en full spole i systemets valuta.",
"price": "Pris för en full spole.",
"weight": "Vikten på en full spole (nettovikt). Detta ska inte inkludera vikten på spolen själv, bara filamentet. Det är vad som vanligtvis står på förpackningen.",
"spool_weight": "Vikten på en tom spole. Används för att beräkna filamentvikten från en vägning.",
"article_number": "T.ex. EAN, UPC, etc."
"article_number": "T.ex. EAN, UPC, etc.",
"multi_color_direction": "Filament kan ha flera färger på två sätt: antingen genom coextrudering, så som två-färgs filament med konsekventa multifärger, eller genom longitudinella färgförändringar, så som gradientfilament som skiftar färger längs spolen."
},
"titles": {
"create": "Skapa filament",
@@ -215,30 +256,37 @@
"show_title": "[Filament #{{id}}] {{name}}"
},
"form": {
"filament_updated": "Det här filamentet har uppdaterats av något eller någon sedan du öppnade den här sidan. Om du sparar så skriver du över ändringarna!"
"filament_updated": "Det här filamentet har uppdaterats av något eller någon sedan du öppnade den här sidan. Om du sparar så skriver du över ändringarna!",
"import_external": "Importera från externt",
"import_external_description": "Välj ett filament i listan för att automatiskt fylla dess detaljer i formuläret. Detta kommer att skriva över alla data som du har angett i formuläret.<br><br>Ett tillverkarobjekt skapas automatiskt när du klickar på Ok, om det behövs."
},
"buttons": {
"add_spool": "Skapa Spole"
}
},
"vendor": {
"vendor": "Leverantörer",
"vendor": "Tillverkare",
"fields": {
"id": "ID",
"name": "Namn",
"registered": "Registrerad",
"comment": "Kommentar"
"comment": "Kommentar",
"empty_spool_weight": "Tom spolvikt",
"external_id": "Externt ID"
},
"titles": {
"create": "Skapa leverantör",
"clone": "Klona leverantör",
"edit": "Ändra leverantör",
"list": "Leverantörer",
"show": "Visa leverantör",
"show_title": "[Leverantör #{{id}}] {{name}}"
"create": "Skapa tillverkare",
"clone": "Klona tillverkare",
"edit": "Ändra tillverkare",
"list": "Tillverkare",
"show": "Visa tillverkare",
"show_title": "[Tillverkare #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "Den här leverantören har uppdaterats av något eller någon sedan du öppnade den här sidan. Om du sparar så skriver du över ändringarna!"
"vendor_updated": "Den här tillverkaren har uppdaterats av något eller någon sedan du öppnade den här sidan. Om du sparar så skriver du över ändringarna!"
},
"fields_help": {
"empty_spool_weight": "Vikten av en tom spool från denna tillverkare."
}
},
"home": {
@@ -309,11 +357,11 @@
"clone": "#{{id}} Klona spole | Spoolman"
},
"vendor": {
"list": "Leverantörer | Spoolman",
"show": "#{{id}} Visa leverantör | Spoolman",
"edit": "#{{id}} Redigera leverantör | Spoolman",
"create": "Skapa leverantör | Spoolman",
"clone": "#{{id}} Klona leverantör | Spoolman"
"list": "Tillverkare | Spoolman",
"show": "#{{id}} Visa tillverkare | Spoolman",
"edit": "#{{id}} Redigera tillverkare | Spoolman",
"create": "Skapa tillverkare | Spoolman",
"clone": "#{{id}} Klona tillverkare | Spoolman"
},
"help": {
"list": "Hjälp | Spoolman"
@@ -325,7 +373,7 @@
"filament": "Märken av filament. De har egenskaper som namn, material, färg, diameter och mer.",
"spool": "Individuella fysiska spolar av ett specifikt filament."
},
"description": "<title>Hjälp</title><p>Här är några tips för att komma igång.</p><p>Spoolman innehåller 3 olika typer av data:</p><itemsHelp/><p>För att lägga till en ny spole i databasen måste du först skapa ett <filamentCreateLink>Filament</filamentCreateLink>-objekt för den. När det är klart kan du sedan skapa ett <spoolCreateLink>Spol</spoolCreateLink>-objekt för den individuella spolen. Om du sedan köper ytterligare spolar av samma filament kan du bara skapa ytterligare Spol-objekt och återanvända samma Filament-objekt.</p><p>Du kan valfritt också skapa ett <vendorCreateLink>Leverantörs</vendorCreateLink>-objekt för företaget som tillverkar filamentet om du vill spåra den informationen.</p><p>Du kan koppla andra 3D-skrivartjänster till Spoolman, som Moonraker, som sedan automatiskt kan spåra filamentanvändning och uppdatera Spol-objekten åt dig. Se <readmeLink>Spoolman README</readmeLink> för hur du gör det.</p>",
"description": "<title>Hjälp</title><p>Här är några tips för att komma igång.</p><p>Spoolman innehåller 3 olika typer av data:</p><itemsHelp/><p>För att lägga till en ny spole i databasen måste du först skapa ett <filamentCreateLink>Filament</filamentCreateLink>-objekt för den. När det är klart kan du sedan skapa ett <spoolCreateLink>Spol</spoolCreateLink>-objekt för den individuella spolen. Om du sedan köper ytterligare spolar av samma filament kan du bara skapa ytterligare Spol-objekt och återanvända samma Filament-objekt.</p><p>Du kan valfritt också skapa ett <vendorCreateLink>Tillverkar</vendorCreateLink>-objekt för företaget som tillverkar filamentet om du vill spåra den informationen.</p><p>Du kan koppla andra 3D-skrivartjänster till Spoolman, som Moonraker, som sedan automatiskt kan spåra filamentanvändning och uppdatera Spol-objekten åt dig. Se <readmeLink>Spoolman README</readmeLink> för hur du gör det.</p>",
"help": "Hjälp"
},
"kofi": "Dricksa mig på Ko-fi"

View File

@@ -65,7 +65,7 @@
"lot_nr": "Номер партії виробника. Може використовуватися для гарантування рівномірного кольору друку при використанні декількох котушок.",
"remaining_weight": "Скільки філаменту залишилося на котушці. Для нової котушки це має відповідати вазі філаменту на котушці.",
"location": "Де розташована котушка, якщо у вас є кілька місць для зберігання котушок.",
"price": "Ціна цілої котушки у налаштованій в системі валюті. Якщо не вказано, замість неї буде використано ціну філамента."
"price": "Ціна цілої котушки. Якщо не вказано, замість неї буде використано ціну філамента."
},
"messages": {
"archive": "Ви впевнені що хочете архівувати цю котушку?"
@@ -199,7 +199,7 @@
"create": "Створити Філамент"
},
"fields_help": {
"price": "Ціна всієї котушки у валюті, встановленій в системі.",
"price": "Ціна всієї котушки.",
"material": "Наприклад PLA, ABS, PETG, тощо.",
"name": "Назва філаменту, щоб відрізнити цей філамент поміж інших від того самого виробника. Вона повинна містити, наприклад, колір.",
"article_number": "Напр. EAN, UPC тощо.",
@@ -209,6 +209,9 @@
"filament": "Філаменти",
"form": {
"filament_updated": "Цей філамент хтось/щось оновив після того, як ви відкрили цю сторінку. Збереження перезапише ці зміни!"
},
"buttons": {
"add_spool": "Додати Котушку"
}
},
"unknown": "Невідомий",
@@ -324,6 +327,7 @@
"delete_confirm_description": "Це призведе до видалення поля та усіх пов'язаних з ним даних для усіх об'єктів.",
"choices_missing_error": "Ви не можете видалити обрані варіанти. Наступні варіанти відсутні: {{choices}}",
"description": "<p>Тут ви можете додати додаткові користувацькі поля до своїх об’єктів.</p><p>Після додавання поля ви не можете змінити його ключ або тип, а для полів типу вибору ви не можете видалити варіанти або змінити стан кількох варіантів. Якщо ви вилучите поле, пов’язані дані для всіх об’єктів буде видалено.</p><p>Ключ це те, як інші програми читають/записують дані, тож якщо ваше спеціальне поле має інтегруватися зі сторонніми програми, переконайтеся, що ви правильно її налаштували. Значення за замовчуванням застосовується лише до нових елементів.</p><p>Додаткові поля не можна сортувати чи фільтрувати в режимі перегляду таблиці.</p>"
}
},
"settings": "Налаштування"
}
}

View File

@@ -41,7 +41,9 @@
"deleteError": "删除 {{resource}} 时出现错误 (状态码:{{statusCode}})",
"editSuccess": "成功修改 {{resource}}",
"editError": "修改 {{resource}} 时出现错误 (状态码:{{statusCode}})",
"importProgress": "导入中: {{processed}}/{{total}}"
"importProgress": "导入中: {{processed}}/{{total}}",
"saveSuccessful": "保存成功!",
"validationError": "校验错误:{{error}}"
},
"loading": "加载中",
"version": "版本",
@@ -96,14 +98,14 @@
"extruderTemp": "挤出温度:{{temp}}",
"textSize": "内容文本大小",
"showSpoolmanIcon": "显示Spoolman图标",
"showVendor": "供应商",
"showVendor": "制造商",
"showContent": "打印标签",
"showLotNr": "批号",
"showSpoolWeight": "料盘重量",
"showTemperatures": "温度",
"showSpoolComment": "料盘备注",
"showFilamentComment": "耗材备注",
"showVendorComment": "供应商备注"
"showVendorComment": "制造商备注"
},
"spoolSelect": {
"title": "选择料盘",
@@ -146,7 +148,11 @@
"registered": "登记时间",
"comment": "备注",
"archived": "存档",
"price": "价格"
"price": "价格",
"spool_weight": "空料盘重量",
"initial_weight": "初始重量",
"filament_internal": "内部",
"filament_external": "外部"
},
"fields_help": {
"weight_to_use": "请选择要输入的重量值。净重只在所选的耗材设置了料盘重量时可用。",
@@ -155,7 +161,10 @@
"measured_weight": "耗材和料盘的重量分别是多少。",
"location": "如果您有多个存放料盘的地方,请填写料盘所在的位置。",
"lot_nr": "制造商的批号。如果使用多个料盘可以使用批号进行控制,以确保打印颜色一致。",
"price": "以系统配置的货币计价的整卷料盘的价格。如果未设置,则默认使用该耗材的价格。"
"price": "整卷料盘的价格。如果未设置,则默认使用该耗材的价格。",
"spool_weight": "料盘没料时的重量。留空时使用耗材或者制造商的相关参数。",
"external_filament": "您已从外部数据库中选择了一种耗材。在创建这个料盘时,将自动创建一个耗材对象(可能还包括一个制造商对象)。如果您已经为这种耗材创建了一个耗材对象,这可能会导致耗材对象重复。",
"initial_weight": "料盘上耗材的初始重量(净重)。如果未设置将使用耗材的相关参数。"
},
"titles": {
"create": "创建料盘",
@@ -178,8 +187,8 @@
"filament": "耗材",
"fields": {
"id": "编号",
"vendor_name": "供应商",
"vendor": "供应商",
"vendor_name": "制造商",
"vendor": "制造商",
"name": "名称",
"material": "材料",
"price": "价格",
@@ -193,10 +202,11 @@
"settings_extruder_temp": "挤出头温度",
"settings_bed_temp": "热床温度",
"color_hex": "颜色",
"spools": "显示料盘"
"spools": "显示料盘",
"external_id": "外部ID"
},
"fields_help": {
"name": "耗材名称,用于区分来自同一供应商的不同耗材类型。例如包含颜色信息。",
"name": "耗材名称,用于区分来自同一制造商的不同耗材类型。例如在耗材名称中包含颜色信息。",
"material": "例如PLA、ABS、PETG等。",
"price": "一整卷耗材的价格。",
"weight": "一整卷耗材的重量(净重)。不应包括料盘本身的重量,仅计算耗材的重量。通常在包装上注明。",
@@ -212,27 +222,37 @@
"show_title": "[耗材 #{{id}}] {{name}}"
},
"form": {
"filament_updated": "自您打开此页面以来,此耗材已被其他人/事物更新。保存将覆盖这些更改!"
"filament_updated": "自您打开此页面以来,此耗材已被其他人/事物更新。保存将覆盖这些更改!",
"import_external_description": "在列表中选择一种耗材以自动填充耗材创建表单中的详细信息。这将覆盖您在表单中输入的任何数据。<br><br>如果有必要,在您点击“确定”时将自动创建一个制造商对象。",
"import_external": "从外部导入"
},
"buttons": {
"add_spool": "添加料盘"
}
},
"vendor": {
"vendor": "供应商",
"vendor": "制造商",
"fields": {
"id": "编号",
"name": "名称",
"registered": "登记时间",
"comment": "备注"
"comment": "备注",
"external_id": "外部ID",
"empty_spool_weight": "空料盘重量"
},
"titles": {
"create": "创建供应商",
"clone": "克隆供应商",
"edit": "编辑供应商",
"list": "供应商列表",
"show": "查看供应商",
"show_title": "[供应商 #{{id}}] {{name}}"
"create": "创建制造商",
"clone": "克隆制造商",
"edit": "编辑制造商",
"list": "制造商列表",
"show": "查看制造商",
"show_title": "[制造商 #{{id}}] {{name}}"
},
"form": {
"vendor_updated": "自您打开此页面以来,此供应商已被其他人/事物更新。保存将覆盖这些更改!"
"vendor_updated": "自您打开此页面以来,此制造商已被其他人/事物更新。保存将覆盖这些更改!"
},
"fields_help": {
"empty_spool_weight": "来自此制造商的空料盘重量。"
}
},
"table": {
@@ -256,11 +276,11 @@
"clone": "#{{id}} 克隆料盘 | Spoolman"
},
"vendor": {
"list": "供应商 | Spoolman",
"show": "#{{id}} 查看供应商 | Spoolman",
"edit": "#{{id}} 编辑供应商 | Spoolman",
"create": "创建供应商 | Spoolman",
"clone": "#{{id}} 克隆供应商 | Spoolman"
"list": "制造商 | Spoolman",
"show": "#{{id}} 查看制造商 | Spoolman",
"edit": "#{{id}} 编辑制造商 | Spoolman",
"create": "创建制造商 | Spoolman",
"clone": "#{{id}} 克隆制造商 | Spoolman"
},
"home": {
"list": "主页 | Spoolman"
@@ -280,6 +300,47 @@
"vendor": "制造耗材的公司。",
"spool": "特定耗材的单个物理卷轴。"
},
"description": "<title>帮助</title><p>以下是一些入门提示。</p><p>Spoolman保存了3种不同类型的数据</p><itemsHelp/><p>要向数据库添加新的料盘,请先创建一个 <filamentCreateLink>耗材</filamentCreateLink> 对象。完成后,继续为该特定耗材创建一个 <spoolCreateLink>料盘</spoolCreateLink> 对象。如果以后获取相同耗材的其他料盘,只需生成其他的料盘对象并重复使用同一个耗材对象。</p><p>同时,如果您希望跟踪该信息,还可以为制造耗材的公司生成一个 <vendorCreateLink>供应商</vendorCreateLink> 对象。</p><p>您可以选择将其他3D打印机服务链接到 Spoolman例如 Moonraker它可以自动监视耗材使用情况并为您更新料盘对象。请参阅 <readmeLink>Spoolman README</readmeLink> 获取有关如何设置的指导。</p>"
"description": "<title>帮助</title><p>以下是一些入门提示。</p><p>Spoolman保存了3种不同类型的数据</p><itemsHelp/><p>要向数据库添加新的料盘,请先创建一个 <filamentCreateLink>耗材</filamentCreateLink> 对象。完成后,继续为该特定耗材创建一个 <spoolCreateLink>料盘</spoolCreateLink> 对象。如果以后获取相同耗材的其他料盘,只需生成其他的料盘对象并重复使用同一个耗材对象。</p><p>同时,如果您希望跟踪该信息,还可以为制造耗材的公司生成一个 <vendorCreateLink>制造商</vendorCreateLink> 对象。</p><p>您可以选择将其他3D打印机服务链接到 Spoolman例如 Moonraker它可以自动监视耗材使用情况并为您更新料盘对象。请参阅 <readmeLink>Spoolman README</readmeLink> 获取有关如何设置的指导。</p>"
},
"settings": {
"settings": "设置",
"extra_fields": {
"tab": "额外字段",
"field_type": {
"integer_range": "整数范围",
"float": "浮点数",
"float_range": "浮点数范围",
"boolean": "布尔类型",
"datetime": "日期时间",
"text": "文本",
"integer": "整数",
"choice": "选项"
},
"delete_confirm_description": "这将删除该字段以及所有实体的关联数据。",
"description": "<p>这里你可以为你的实体添加额外的自定义字段。</p><p>一旦添加了字段,就无法更改其键或类型;对于选择类型字段,无法删除选项或更改多选状态。如果删除字段,则所有实体的相关数据将被删除。</p><p>键是其他程序读取/写入数据的标识,因此,如果你的自定义字段需要与第三方程序集成,请确保设置正确。默认值仅适用于新项目。</p><p>额外字段在表视图中无法进行排序或过滤。</p>",
"params": {
"name": "名称",
"field_type": "类型",
"order": "顺序",
"default_value": "默认值",
"multi_choice": "多选",
"choices": "选择项",
"key": "键",
"unit": "单位"
},
"boolean_true": "是",
"boolean_false": "否",
"choices_missing_error": "您无法删除选择。以下选项丢失:{{choices}}",
"non_unique_key_error": "键必须是唯一的。",
"key_not_changed": "请将键改成其他的。",
"delete_confirm": "删除字段 {{name}}"
},
"header": "设置",
"general": {
"tab": "常规",
"currency": {
"label": "货币"
}
}
}
}

View File

@@ -5,11 +5,6 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ErrorComponent } from "@refinedev/antd";
import "@refinedev/antd/dist/reset.css";
import routerBindings, { DocumentTitleHandler, UnsavedChangesNotifier } from "@refinedev/react-router-v6";
import dataProvider from "./components/dataProvider";
import { useTranslation } from "react-i18next";
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
import { ColorModeContextProvider } from "./contexts/color-mode";
import {
FileOutlined,
HighlightOutlined,
@@ -18,14 +13,21 @@ import {
ToolOutlined,
UserOutlined,
} from "@ant-design/icons";
import { ConfigProvider } from "antd";
import React from "react";
import { Locale } from "antd/es/locale";
import { languages } from "./i18n";
import loadable from "@loadable/component";
import SpoolmanNotificationProvider from "./components/notificationProvider";
import routerBindings, { DocumentTitleHandler, UnsavedChangesNotifier } from "@refinedev/react-router-v6";
import { ConfigProvider } from "antd";
import { Locale } from "antd/es/locale";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
import dataProvider from "./components/dataProvider";
import { Favicon } from "./components/favicon";
import { SpoolmanLayout } from "./components/layout";
import liveProvider from "./components/liveProvider";
import SpoolmanNotificationProvider from "./components/notificationProvider";
import { ColorModeContextProvider } from "./contexts/color-mode";
import { languages } from "./i18n";
import { getAPIURL, getBasePath } from "./utils/url";
interface ResourcePageProps {
resource: "spools" | "filaments" | "vendors";
@@ -60,8 +62,8 @@ function App() {
};
// Fetch the antd locale using dynamic imports
const [antdLocale, setAntdLocale] = React.useState<Locale | undefined>();
React.useEffect(() => {
const [antdLocale, setAntdLocale] = useState<Locale | undefined>();
useEffect(() => {
const fetchLocale = async () => {
const locale = await import(
`./../node_modules/antd/es/locale/${languages[i18n.language].fullCode.replace("-", "_")}.js`
@@ -84,7 +86,7 @@ function App() {
}
return (
<BrowserRouter>
<BrowserRouter basename={getBasePath() + "/"}>
<RefineKbarProvider>
<ColorModeContextProvider>
<ConfigProvider
@@ -96,11 +98,11 @@ function App() {
}}
>
<Refine
dataProvider={dataProvider(import.meta.env.VITE_APIURL)}
dataProvider={dataProvider(getAPIURL())}
notificationProvider={SpoolmanNotificationProvider}
i18nProvider={i18nProvider}
routerProvider={routerBindings}
liveProvider={liveProvider(import.meta.env.VITE_APIURL)}
liveProvider={liveProvider(getAPIURL())}
resources={[
{
name: "home",
@@ -190,6 +192,7 @@ function App() {
/>
<Route path="edit/:id" element={<LoadableResourcePage resource="spools" page="edit" />} />
<Route path="show/:id" element={<LoadableResourcePage resource="spools" page="show" />} />
<Route path="print" element={<LoadablePage name="printing" />} />
</Route>
<Route path="/filament">
<Route index element={<LoadableResourcePage resource="filaments" page="list" />} />
@@ -227,6 +230,7 @@ function App() {
<UnsavedChangesNotifier />
<DocumentTitleHandler />
<ReactQueryDevtools />
<Favicon url={getBasePath() + "/favicon.svg"} />
</Refine>
</ConfigProvider>
</ColorModeContextProvider>

View File

@@ -1,19 +1,18 @@
import { DateField, TextField } from "@refinedev/antd";
import { UseQueryResult } from "@tanstack/react-query";
import { Button, Col, Dropdown, Row, Space, Spin } from "antd";
import { ColumnFilterItem, ColumnType } from "antd/es/table/interface";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { AlignType } from "rc-table/lib/interface";
import { Link } from "react-router-dom";
import { getFiltersForField, typeFilters } from "../utils/filtering";
import { enrichText } from "../utils/parsing";
import { Field, FieldType } from "../utils/queryFields";
import { TableState } from "../utils/saveload";
import { getSortOrderForField, typeSorters } from "../utils/sorting";
import { NumberFieldUnit, NumberFieldUnitRange } from "./numberField";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { DateField, TextField } from "@refinedev/antd";
import Icon from "@ant-design/icons";
import SpoolIcon from "../icon_spool.svg?react";
import { useTranslate } from "@refinedev/core";
import { enrichText } from "../utils/parsing";
import { UseQueryResult } from "@tanstack/react-query";
import { Link } from "react-router-dom";
import { Field, FieldType } from "../utils/queryFields";
import SpoolIcon from "./spoolIcon";
dayjs.extend(utc);
@@ -45,6 +44,7 @@ interface BaseColumnProps<Obj extends Entity> {
i18ncat?: string;
i18nkey?: string;
title?: string;
align?: AlignType;
sorter?: boolean;
t: (key: string) => string;
navigate: (link: string) => void;
@@ -88,6 +88,7 @@ function Column<Obj extends Entity>(
const columnProps: ColumnType<Obj> = {
dataIndex: props.id,
align: props.align,
title: props.title ?? t(props.i18nkey ?? `${props.i18ncat}.fields.${props.id}`),
filterMultiple: props.allowMultipleFilters ?? true,
width: props.width ?? undefined,
@@ -196,7 +197,7 @@ export function FilteredQueryColumn<Obj extends Entity>(props: FilteredQueryColu
if (typeof item === "string") {
return {
text: item,
value: item,
value: '"' + item + '"',
};
}
return item;
@@ -227,6 +228,7 @@ interface NumberColumnProps<Obj extends Entity> extends BaseColumnProps<Obj> {
export function NumberColumn<Obj extends Entity>(props: NumberColumnProps<Obj>) {
return Column({
...props,
align: "right",
render: (rawValue) => {
const value = props.transform ? props.transform(rawValue) : rawValue;
if (value === null || value === undefined) {
@@ -256,18 +258,19 @@ export function DateColumn<Obj extends Entity>(props: BaseColumnProps<Obj>) {
hidden={!value}
value={dayjs.utc(value).local()}
title={dayjs.utc(value).local().format()}
format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm"
/>
);
},
});
}
export function ActionsColumn<Obj extends Entity>(actionsFn: (record: Obj) => Action[]): ColumnType<Obj> | undefined {
const t = useTranslate();
export function ActionsColumn<Obj extends Entity>(
title: string,
actionsFn: (record: Obj) => Action[]
): ColumnType<Obj> | undefined {
return {
title: t("table.actions"),
title,
responsive: ["lg"],
render: (_, record) => {
const buttons = actionsFn(record).map((action) => {
@@ -296,7 +299,7 @@ export function ActionsColumn<Obj extends Entity>(actionsFn: (record: Obj) => Ac
}
interface SpoolIconColumnProps<Obj extends Entity> extends FilteredQueryColumnProps<Obj> {
color: (record: Obj) => string | undefined;
color: (record: Obj) => string | { colors: string[]; vertical: boolean } | undefined;
}
export function SpoolIconColumn<Obj extends Entity>(props: SpoolIconColumnProps<Obj>) {
@@ -308,7 +311,7 @@ export function SpoolIconColumn<Obj extends Entity>(props: SpoolIconColumnProps<
if (typeof item === "string") {
return {
text: item,
value: item,
value: '"' + item + '"',
};
}
return item;
@@ -343,19 +346,12 @@ export function SpoolIconColumn<Obj extends Entity>(props: SpoolIconColumnProps<
},
render: (rawValue, record: Obj) => {
const value = props.transform ? props.transform(rawValue) : rawValue;
const colorStr = props.color(record);
const colorObj = props.color(record);
return (
<Row wrap={false} justify="space-around" align="middle">
{colorStr && (
{colorObj && (
<Col flex="none">
<Icon
component={SpoolIcon}
style={{
color: "#" + colorStr,
fontSize: 42,
marginRight: 0,
}}
/>
<SpoolIcon color={colorObj} />
</Col>
)}
<Col flex="auto">{value}</Col>

View File

@@ -1,7 +1,7 @@
import { AxiosInstance } from "axios";
import { stringify } from "query-string";
import { DataProvider } from "@refinedev/core";
import { axiosInstance } from "@refinedev/simple-rest";
import { AxiosInstance } from "axios";
import { stringify } from "query-string";
type MethodTypes = "get" | "delete" | "head" | "options";
type MethodTypesWithBody = "post" | "put" | "patch";

View File

@@ -1,8 +1,8 @@
import { DatePicker } from "antd";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import advancedFormat from "dayjs/plugin/advancedFormat";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
dayjs.extend(utc);
dayjs.extend(timezone);

View File

@@ -1,13 +1,13 @@
import { DateField, TextField } from "@refinedev/antd";
import { Field, FieldType } from "../utils/queryFields";
import { NumberFieldUnit, NumberFieldUnitRange } from "./numberField";
import { enrichText } from "../utils/parsing";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { Checkbox, Form, Input, InputNumber, Select, Typography } from "antd";
import { FormItemProps, Rule } from "antd/es/form";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { enrichText } from "../utils/parsing";
import { Field, FieldType } from "../utils/queryFields";
import { DateTimePicker } from "./dateTimePicker";
import { InputNumberRange } from "./inputNumberRange";
import { NumberFieldUnit, NumberFieldUnitRange } from "./numberField";
dayjs.extend(utc);

View File

@@ -0,0 +1,20 @@
import { useEffect } from "react";
/**
* Renders a favicon element in the head of the HTML document with the specified URL.
*
* @param {string} props.url - The URL of the favicon image.
* @return {JSX.Element} - An empty JSX element.
*/
export function Favicon(props: { url: string }) {
useEffect(() => {
let link = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
if (!link) {
link = document.createElement("link") as HTMLLinkElement;
link.rel = "icon";
document.getElementsByTagName("head")[0].appendChild(link);
}
link.href = props.url;
}, [props.url]);
return <></>;
}

View File

@@ -0,0 +1,72 @@
import { useTranslate } from "@refinedev/core";
import { Form, Modal, Select } from "antd";
import { Trans } from "react-i18next";
import { formatFilamentLabel } from "../pages/spools/functions";
import { searchMatches } from "../utils/filtering";
import { ExternalFilament, useGetExternalDBFilaments } from "../utils/queryExternalDB";
export function FilamentImportModal(props: {
isOpen: boolean;
onImport: (filament: ExternalFilament) => void;
onClose: () => void;
}) {
const [form] = Form.useForm();
const t = useTranslate();
const externalFilaments = useGetExternalDBFilaments();
const filamentOptions =
externalFilaments.data?.map((item) => {
return {
label: formatFilamentLabel(
item.name,
item.diameter,
item.manufacturer,
item.material,
item.weight,
item.spool_type
),
value: item.id,
item: item,
};
}) ?? [];
filamentOptions.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
return (
<Modal
title={t("filament.form.import_external")}
open={props.isOpen}
onOk={() => form.submit()}
onCancel={() => props.onClose()}
>
<Form
layout="vertical"
form={form}
onFinish={(values) => {
const filament = filamentOptions.find((item) => item.value === values.filament)?.item;
if (!filament) {
throw new Error("Filament not found");
}
props.onImport(filament);
props.onClose();
form.resetFields();
}}
>
<p>
<Trans
i18nKey={"filament.form.import_external_description"}
components={{
br: <br />,
}}
/>
</p>
<Form.Item name="filament" rules={[{ required: true }]}>
<Select
options={filamentOptions}
showSearch
filterOption={(input, option) => typeof option?.label === "string" && searchMatches(input, option?.label)}
/>
</Form.Item>
</Form>
</Modal>
);
}

View File

@@ -1,13 +1,13 @@
import { DownOutlined } from "@ant-design/icons";
import type { RefineThemedLayoutV2HeaderProps } from "@refinedev/antd";
import { useGetLocale, useSetLocale } from "@refinedev/core";
import { Button, Dropdown, Layout as AntdLayout, MenuProps, Space, Switch, theme } from "antd";
import { Layout as AntdLayout, Button, Dropdown, MenuProps, Space, Switch, theme } from "antd";
import React, { useContext } from "react";
import { ColorModeContext } from "../../contexts/color-mode";
import "/node_modules/flag-icons/css/flag-icons.min.css";
import { languages } from "../../i18n";
import QRCodeScannerModal from "../qrCodeScanner";
import "/node_modules/flag-icons/css/flag-icons.min.css";
const { useToken } = theme;

View File

@@ -1,10 +1,11 @@
import { ThemedLayoutV2, ThemedSiderV2, ThemedTitleV2 } from "@refinedev/antd";
import { Header } from "./header";
import { Footer } from "antd/es/layout/layout";
import { Version } from "./version";
import { Button } from "antd";
import Logo from "../icon.svg?react";
import { useTranslate } from "@refinedev/core";
import { Button } from "antd";
import { Footer } from "antd/es/layout/layout";
import Logo from "../icon.svg?react";
import { getBasePath } from "../utils/url";
import { Header } from "./header";
import { Version } from "./version";
const SpoolmanFooter: React.FC = () => {
const t = useTranslate();
@@ -27,7 +28,7 @@ const SpoolmanFooter: React.FC = () => {
<Button
icon={
<img
src="/kofi_s_logo_nolabel.png"
src={getBasePath() + "/kofi_s_logo_nolabel.png"}
style={{
height: "1.6em",
}}

View File

@@ -1,8 +1,9 @@
import { LiveEvent } from "@refinedev/core";
import React from "react";
import { useEffect, useState } from "react";
import { getAPIURL } from "../utils/url";
import liveProvider from "./liveProvider";
const liveProviderInstance = liveProvider(import.meta.env.VITE_APIURL);
const liveProviderInstance = liveProvider(getAPIURL());
/**
* Hook that subscribes to live updates for the items in the dataSource
@@ -15,29 +16,23 @@ export function useLiveify<Data extends { id: number }>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
transformPayload: (payload: any) => Data
) {
// TODO: The hooks in this function is quite janky, and should be refactored to be more efficient
// New state that holds the dataSource with updated values from the live provider
const [updatedDataSource, setUpdatedDataSource] = React.useState<Data[]>(dataSource);
const [updatedDataSource, setUpdatedDataSource] = useState<Data[]>(dataSource);
// If the original dataSource changes, update the updatedDataSource
React.useEffect(() => {
useEffect(() => {
setUpdatedDataSource(dataSource);
}, [dataSource]);
// Create a constant reference to itemIds. This is to prevent the useEffect below from triggering extra times.
const itemIds = dataSource.map((item) => item.id);
const [prevItemIds, setPrevItemIds] = React.useState<number[]>(itemIds);
if (JSON.stringify(itemIds) !== JSON.stringify(prevItemIds)) {
setPrevItemIds(itemIds);
}
// Subscribe to changes for all items in the dataSource
React.useEffect(() => {
useEffect(() => {
const itemIds = dataSource.map((item) => item.id);
const subscription = liveProviderInstance?.subscribe({
channel: `${resource}-list`,
params: {
resource: resource,
ids: prevItemIds,
ids: itemIds,
subscriptionType: "useList",
},
types: ["update"],
@@ -56,7 +51,7 @@ export function useLiveify<Data extends { id: number }>(
liveProviderInstance?.unsubscribe(subscription);
}
};
}, [resource, prevItemIds, transformPayload]);
}, [resource, dataSource, transformPayload]);
return updatedDataSource;
}

View File

@@ -0,0 +1,83 @@
import { CloseOutlined, PlusOutlined } from "@ant-design/icons";
import { Badge, Button, ColorPicker, Space } from "antd";
function generateRandomColor() {
return "000000".replace(/0/g, function () {
return (~~(Math.random() * 16)).toString(16);
});
}
function generateInitialColors(num: number) {
const colors = [];
for (let i = 0; i < num; i++) {
colors.push(generateRandomColor());
}
return colors;
}
/**
* An Ant Design compatible form input for multiple color pickers
* The value is a comma separated list of hex values, without hashtags
* @param props
* @returns
*/
export function MultiColorPicker(props: {
value?: string | null | undefined;
onChange?: (value: string | null | undefined) => void;
min?: number;
max?: number;
}) {
const values = props.value ? props.value.split(",") : generateInitialColors(props.min ?? 0);
if (!props.value && props.onChange) {
// Update value immediately
props.onChange(values.join(","));
}
const pickers = values.map((value, idx) => (
<Badge
key={idx}
count={
values.length > (props.min ?? 0) ? (
<span className="ant-badge-count">
<CloseOutlined
onClick={() => {
// Remove this picker
if (props.onChange) {
props.onChange(values.filter((v, i) => i !== idx).join(","));
}
}}
/>
</span>
) : (
<></>
)
}
>
<ColorPicker
value={value}
onChange={(clr) => {
if (props.onChange) {
props.onChange(values.map((v, i) => (i === idx ? clr.toHex() : v)).join(","));
}
}}
/>
</Badge>
));
return (
<>
<Space direction="horizontal" size="middle" style={{ marginTop: "1em" }}>
{pickers}
{values.length < (props.max ?? Infinity) && (
<Button
icon={<PlusOutlined />}
onClick={() => {
if (props.onChange) {
props.onChange(values.concat(generateRandomColor()).join(","));
}
}}
/>
)}
</Space>
</>
);
}

View File

@@ -1,7 +1,40 @@
import { NotificationProvider } from "@refinedev/core";
import { message } from "antd";
import { UndoOutlined } from "@ant-design/icons";
import { NotificationProvider, OpenNotificationParams } from "@refinedev/core";
import { Button, Progress, message } from "antd";
import { UndoableNotification } from "@refinedev/antd/src/components/undoableNotification";
type UndoableNotificationProps = {
notificationKey: OpenNotificationParams["key"];
message: OpenNotificationParams["message"];
cancelMutation: OpenNotificationParams["cancelMutation"];
undoableTimeout: OpenNotificationParams["undoableTimeout"];
};
const UndoableNotification: React.FC<UndoableNotificationProps> = ({ message, cancelMutation, undoableTimeout }) => (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginTop: "-7px",
}}
>
<Progress
type="circle"
percent={(undoableTimeout ?? 0) * 20}
format={(time) => time && time / 20}
size={50}
strokeColor="#1890ff"
status="normal"
/>
<span style={{ marginLeft: 8, width: "100%" }}>{message}</span>
<Button
style={{ flexShrink: 0 }}
onClick={cancelMutation}
disabled={undoableTimeout === 0}
icon={<UndoOutlined />}
></Button>
</div>
);
const SpoolmanNotificationProvider: NotificationProvider = {
open: ({ key, message: content, type, cancelMutation, undoableTimeout }) => {

View File

@@ -1,6 +1,6 @@
import React from "react";
import { Typography } from "antd";
import { NumberFieldProps } from "@refinedev/antd/dist/components/fields/types";
import { Typography } from "antd";
import React from "react";
const { Text } = Typography;

View File

@@ -1,16 +1,16 @@
import { useQuery } from "@tanstack/react-query";
import { Tooltip } from "antd";
import { ColumnFilterItem } from "antd/es/table/interface";
import { IFilament } from "../pages/filaments/model";
import { IVendor } from "../pages/vendors/model";
import { ColumnFilterItem } from "antd/es/table/interface";
import { Tooltip } from "antd";
import { getAPIURL } from "../utils/url";
export function useSpoolmanFilamentFilter(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<IFilament[], unknown, ColumnFilterItem[]>({
enabled: enabled,
queryKey: ["filaments"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/filament");
const response = await fetch(getAPIURL() + "/filament");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -86,12 +86,11 @@ export function useSpoolmanFilamentFilter(enabled: boolean = false) {
}
export function useSpoolmanFilamentNames(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<IFilament[], unknown, string[]>({
enabled: enabled,
queryKey: ["filaments"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/filament");
const response = await fetch(getAPIURL() + "/filament");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -115,12 +114,11 @@ export function useSpoolmanFilamentNames(enabled: boolean = false) {
}
export function useSpoolmanVendors(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<IVendor[], unknown, string[]>({
enabled: enabled,
queryKey: ["vendors"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/vendor");
const response = await fetch(getAPIURL() + "/vendor");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -137,12 +135,11 @@ export function useSpoolmanVendors(enabled: boolean = false) {
}
export function useSpoolmanMaterials(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<string[]>({
enabled: enabled,
queryKey: ["materials"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/material");
const response = await fetch(getAPIURL() + "/material");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -155,12 +152,11 @@ export function useSpoolmanMaterials(enabled: boolean = false) {
}
export function useSpoolmanArticleNumbers(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<string[]>({
enabled: enabled,
queryKey: ["articleNumbers"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/article-number");
const response = await fetch(getAPIURL() + "/article-number");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -173,12 +169,11 @@ export function useSpoolmanArticleNumbers(enabled: boolean = false) {
}
export function useSpoolmanLotNumbers(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<string[]>({
enabled: enabled,
queryKey: ["lotNumbers"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/lot-number");
const response = await fetch(getAPIURL() + "/lot-number");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -191,12 +186,11 @@ export function useSpoolmanLotNumbers(enabled: boolean = false) {
}
export function useSpoolmanLocations(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<string[]>({
enabled: enabled,
queryKey: ["locations"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/location");
const response = await fetch(getAPIURL() + "/location");
if (!response.ok) {
throw new Error("Network response was not ok");
}

View File

@@ -1,129 +0,0 @@
import { Col, Form, InputNumber, QRCode, Row, Slider, Switch } from "antd";
import { useSavedState } from "../../utils/saveload";
import PrintingDialog from "./printingDialog";
import { useTranslate } from "@refinedev/core";
interface QRCodeData {
value: string;
label?: JSX.Element;
errorLevel?: "L" | "M" | "Q" | "H";
}
interface QRCodePrintingDialogProps {
visible: boolean;
items: QRCodeData[];
onCancel: () => void;
extraSettings?: JSX.Element;
}
const QRCodePrintingDialog: React.FC<QRCodePrintingDialogProps> = ({ visible, items, onCancel, extraSettings }) => {
const t = useTranslate();
const [showContent, setShowContent] = useSavedState("print-showContent", true);
const [textSize, setTextSize] = useSavedState("print-textSize", 3);
const [showSpoolmanIcon, setShowSpoolmanIcon] = useSavedState("print-showSpoolmanIcon", true);
const elements = items.map((item) => {
return (
<div className="print-qrcode-item">
<div className="print-qrcode-container">
<QRCode
className="print-qrcode"
icon={showSpoolmanIcon ? "/favicon.svg" : undefined}
value={item.value}
errorLevel={item.errorLevel}
type="svg"
color="#000"
/>
</div>
{showContent && <div className="print-qrcode-title">{item.label ?? item.value}</div>}
</div>
);
});
return (
<PrintingDialog
visible={visible}
title={t("printing.qrcode.title")}
items={elements}
extraSettings={
<>
<Form.Item label={t("printing.qrcode.showContent")}>
<Switch checked={showContent} onChange={(checked) => setShowContent(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.textSize")}>
<Row>
<Col span={12}>
<Slider
disabled={!showContent}
tooltip={{ formatter: (value) => `${value} mm` }}
min={2}
max={7}
value={textSize}
step={0.1}
onChange={(value) => {
setTextSize(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
disabled={!showContent}
min={0.01}
step={0.1}
style={{ margin: "0 16px" }}
value={textSize}
addonAfter="mm"
onChange={(value) => {
setTextSize(value ?? 5);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.qrcode.showSpoolmanIcon")}>
<Switch checked={showSpoolmanIcon} onChange={(checked) => setShowSpoolmanIcon(checked)} />
</Form.Item>
{extraSettings}
</>
}
style={`
.print-page .print-qrcode-item {
display: flex;
width: 100%;
max-height: 100%;
justify-content: center;
}
.print-page .print-qrcode-container {
max-width: ${showContent ? "50%" : "100%"};
display: flex;
}
.print-page .print-qrcode {
width: auto !important;
height: auto !important;
}
.print-page .print-qrcode-title {
flex: 1 1 auto;
font-size: ${textSize}mm;
color: #000;
overflow: hidden;
}
.print-page canvas, .print-page svg {
/* display: block; */
object-fit: contain;
height: 100% !important;
width: 100% !important;
max-height: 100%;
max-width: 100%;
}
`}
onCancel={onCancel}
/>
);
};
export default QRCodePrintingDialog;

View File

@@ -1,142 +0,0 @@
import { Form, Switch } from "antd";
import { IFilament } from "../../pages/filaments/model";
import { ISpool } from "../../pages/spools/model";
import QRCodePrintingDialog from "./qrCodePrintingDialog";
import { useSavedState } from "../../utils/saveload";
import { useGetSetting } from "../../utils/querySettings";
import { useTranslate } from "@refinedev/core";
interface SpoolQRCodePrintingDialog {
visible: boolean;
items: ISpool[];
onCancel: () => void;
}
const SpoolQRCodePrintingDialog: React.FC<SpoolQRCodePrintingDialog> = ({ visible, items, onCancel }) => {
const t = useTranslate();
const baseUrlSetting = useGetSetting("qr_code_url");
const baseUrlRoot = baseUrlSetting.data?.value !== undefined && JSON.parse(baseUrlSetting.data?.value) !== "" ? JSON.parse(baseUrlSetting.data?.value) : window.location.origin;
const [showVendor, setShowVendor] = useSavedState("print-showVendor", true);
const [showLotNr, setShowLotNr] = useSavedState("print-showLotNr", true);
const [showSpoolWeight, setShowSpoolWeight] = useSavedState("print-showSpoolWeight", true);
const [showTemperatures, setShowTemperatures] = useSavedState("print-showTemperatures", true);
const [showSpoolComment, setShowSpoolComment] = useSavedState("print-showSpoolComment", false);
const [showFilamentComment, setShowFilamentComment] = useSavedState("print-showFilamentComment", false);
const [showVendorComment, setShowVendorComment] = useSavedState("print-showVendorComment", false);
const [useHTTPUrl, setUseHTTPUrl] = useSavedState("print-useHTTPUrl", baseUrlSetting.data?.value !== "");
const formatFilament = (filament: IFilament) => {
let vendorPrefix = "";
if (showVendor && filament.vendor) {
vendorPrefix = `${filament.vendor.name} - `;
}
let name = filament.name;
if (!name) {
name = `Filament #${filament.id}`;
}
return `${vendorPrefix}${name}`;
};
return (
<QRCodePrintingDialog
visible={visible}
onCancel={onCancel}
items={items.map(function (spool) {
const temps = [];
if (spool.filament.settings_extruder_temp) {
temps.push(t("printing.qrcode.extruderTemp", { temp: `${spool.filament.settings_extruder_temp} °C` }));
}
if (spool.filament.settings_bed_temp) {
temps.push(t("printing.qrcode.bedTemp", { temp: `${spool.filament.settings_bed_temp} °C` }));
}
const tempLine = temps.join(" - ");
return {
value: useHTTPUrl ? `${baseUrlRoot}/spool/show/${spool.id}` : `web+spoolman:s-${spool.id}`,
label: (
<p
style={{
padding: "1mm 1mm 1mm 0",
}}
>
<b>{formatFilament(spool.filament)}</b>
<br />
<b>
#{spool.id}
{spool.filament.material && <> - {spool.filament.material}</>}
</b>
{showSpoolWeight && (
<>
<br />
{t("printing.qrcode.spoolWeight", { weight: `${spool.filament.spool_weight ?? "?"} g` })}
</>
)}
{showTemperatures && tempLine && (
<>
<br />
{tempLine}
</>
)}
{showLotNr && (
<>
<br />
{t("printing.qrcode.lotNr", { lot: spool.lot_nr ?? "?" })}
</>
)}
{showSpoolComment && spool.comment && (
<>
<br />
{spool.comment}
</>
)}
{showFilamentComment && spool.filament.comment && (
<>
<br />
{spool.filament.comment}
</>
)}
{showVendorComment && spool.filament.vendor?.comment && (
<>
<br />
{spool.filament.vendor.comment}
</>
)}
</p>
),
errorLevel: "H",
};
})}
extraSettings={
<>
<Form.Item label={t("printing.qrcode.showVendor")}>
<Switch checked={showVendor} onChange={(checked) => setShowVendor(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.showSpoolWeight")}>
<Switch checked={showSpoolWeight} onChange={(checked) => setShowSpoolWeight(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.showTemperatures")}>
<Switch checked={showTemperatures} onChange={(checked) => setShowTemperatures(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.showLotNr")}>
<Switch checked={showLotNr} onChange={(checked) => setShowLotNr(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.showSpoolComment")}>
<Switch checked={showSpoolComment} onChange={(checked) => setShowSpoolComment(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.showFilamentComment")}>
<Switch checked={showFilamentComment} onChange={(checked) => setShowFilamentComment(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.showVendorComment")}>
<Switch checked={showVendorComment} onChange={(checked) => setShowVendorComment(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.useHTTPUrl.label")} tooltip={t("printing.qrcode.useHTTPUrl.tooltip")}>
<Switch checked={useHTTPUrl} onChange={(checked) => setUseHTTPUrl(checked)} />
</Form.Item>
</>
}
/>
);
};
export default SpoolQRCodePrintingDialog;

View File

@@ -1,9 +1,9 @@
import React, { useState } from "react";
import { FloatButton, Modal, Space } from "antd";
import { QrScanner } from "@yudiel/react-qr-scanner";
import { useNavigate } from "react-router-dom";
import { CameraOutlined } from "@ant-design/icons";
import { useTranslate } from "@refinedev/core";
import { QrScanner } from "@yudiel/react-qr-scanner";
import { FloatButton, Modal, Space } from "antd";
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
const QRCodeScannerModal: React.FC = () => {
const [visible, setVisible] = useState(false);

View File

@@ -1,50 +0,0 @@
// React FC that combines the functionality of first selecting what spools to print QR code for, and then opens up the QR code printing dialog.
import React from "react";
import SpoolSelectModal from "./spoolSelectModal";
import { Button } from "antd";
import { ISpool } from "../pages/spools/model";
import { PrinterOutlined } from "@ant-design/icons";
import { useTranslate } from "@refinedev/core";
import SpoolQRCodePrintingDialog from "./printing/spoolQrCodePrintingDialog";
const SelectAndPrint: React.FC = () => {
const t = useTranslate();
const [step, setStep] = React.useState(0);
const [selectedSpools, setSelectedSpools] = React.useState<ISpool[]>([]);
return (
<>
<Button
type="primary"
icon={<PrinterOutlined />}
onClick={() => {
setStep(1);
}}
>
{t("printing.qrcode.button")}
</Button>
<SpoolSelectModal
visible={step === 1}
description={t("printing.spoolSelect.description")}
onCancel={() => {
setStep(0);
}}
onContinue={(spools) => {
setSelectedSpools(spools);
setStep(2);
}}
/>
<SpoolQRCodePrintingDialog
visible={step === 2}
onCancel={() => {
setStep(0);
}}
items={selectedSpools}
/>
</>
);
};
export default SelectAndPrint;

View File

@@ -0,0 +1,41 @@
.spool-icon {
display: flex;
width: 1.5em;
height: 2.5em;
gap: 2px;
margin: 0 0.5em;
}
.spool-icon.vertical {
flex-direction: column;
}
.spool-icon.large {
width: 4em;
height: 4em;
}
.spool-icon * {
flex: 1 1 0px;
border-radius: 2px;
}
.spool-icon.vertical *:first-child {
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
.spool-icon.vertical *:last-child {
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
}
.spool-icon.horizontal *:first-child {
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
}
.spool-icon.horizontal *:last-child {
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}

View File

@@ -0,0 +1,32 @@
import "./spoolIcon.css";
interface Props {
color: string | { colors: string[]; vertical: boolean };
size?: "small" | "large";
}
export default function SpoolIcon(props: Props) {
let dirClass = "vertical";
let cols = [];
let size = props.size ? props.size : "small";
if (typeof props.color === "string") {
cols = [props.color];
} else {
dirClass = props.color.vertical ? "vertical" : "horizontal";
cols = props.color.colors;
}
return (
<div className={"spool-icon " + dirClass + " " + size}>
{cols.map((col) => (
<div
key={col}
style={{
backgroundColor: "#" + col.replace("#", ""),
}}
></div>
))}
</div>
);
}

View File

@@ -1,5 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { Spin, Typography } from "antd";
import { getAPIURL } from "../utils/url";
const { Text } = Typography;
@@ -15,12 +16,10 @@ interface IInfo {
}
export const Version: React.FC = () => {
const apiEndpoint = import.meta.env.VITE_APIURL;
const infoResult = useQuery<IInfo>({
queryKey: ["info"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/info");
const response = await fetch(getAPIURL() + "/info");
if (!response.ok) {
throw new Error("Network response was not ok");
}

View File

@@ -1,7 +1,8 @@
import i18n from "i18next";
import detector from "i18next-browser-languagedetector";
import Backend from "i18next-xhr-backend";
import Backend from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
import { getBasePath } from "./utils/url";
interface Language {
name: string;
@@ -97,6 +98,11 @@ export const languages: { [key: string]: Language } = {
countryCode: "dk",
fullCode: "da-DK",
},
["pt"]: {
name: "Português",
countryCode: "pt",
fullCode: "pt-PT",
},
};
i18n
@@ -106,8 +112,9 @@ i18n
.init({
supportedLngs: Object.keys(languages),
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json",
loadPath: getBasePath() + "/locales/{{lng}}/{{ns}}.json",
},
ns: "common",
defaultNS: "common",
fallbackLng: "en",
});

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"
width="100%" height="100%" viewBox="0 0 235 500" xml:space="preserve">
<desc>Created with Fabric.js 5.3.0</desc>
<defs>
</defs>
<g transform="matrix(0.5875748337 0 0 3.9476902309 197.1354865303 250.0467513161)" id="z-gD78pOtWjQii31Q-6Ze" >
<path style="stroke: rgb(110,11,48); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(52,52,52); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(0, 0)" d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z" stroke-linecap="round" />
</g>
<g transform="matrix(0.3815761198 0 0 3.4623174046 197.1354865303 250.0467382769)" id="qCAOML5j7kpAVIP3Df73i" >
<path style="stroke: rgb(110,11,48); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4;fill-rule: nonzero; opacity: 1;" fill="current" vector-effect="non-scaling-stroke" transform=" translate(0, 0)" d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z" stroke-linecap="round" />
</g>
<g transform="matrix(2.0736420266 0 0 3.3577085856 117.2947215155 250.0467440227)" id="DwEIaQUSPW4DgKfkdPWoC" >
<path style="stroke: rgb(0,0,0); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4;fill-rule: nonzero; opacity: 1;" fill="current" vector-effect="non-scaling-stroke" transform=" translate(-0.000005, 0)" d="M -38.50267 -65.24064 L 38.502680000000005 -65.24064 L 38.502680000000005 65.24064 L -38.50267 65.24064 z" stroke-linecap="round" />
</g>
<g transform="matrix(0.5875748337 0 0 3.9476902309 37.4539565008 250.0467440227)" id="OvgM38-6CZm9R47a1TDIX" >
<filter id="SVGID_90" y="-24.74162191%" height="149.48324381999998%" x="-40.54702828%" width="181.09405656%" >
<feGaussianBlur in="SourceAlpha" stdDeviation="20"></feGaussianBlur>
<feOffset dx="30" dy="0" result="oBlur" ></feOffset>
<feFlood flood-color="rgb(0,0,0)" flood-opacity="0.8"/>
<feComposite in2="oBlur" operator="in" />
<feMerge>
<feMergeNode></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
<path style="stroke: rgb(110,11,48); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(52,52,52); fill-rule: nonzero; opacity: 1;filter: url(#SVGID_90);" vector-effect="non-scaling-stroke" transform=" translate(0, 0)" d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z" stroke-linecap="round" />
</g>
<g transform="matrix(0.2435597948 0 0 1.6363849628 37.4539565008 250.0467440227)" id="woBV3Wt7ijQ6puNeMgAQD" >
<path style="stroke: rgb(110,11,48); stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(17,17,17); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(0, 0)" d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z" stroke-linecap="round" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,16 +1,20 @@
import React from "react";
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Create, useForm, useSelect } from "@refinedev/antd";
import { Form, Input, Select, InputNumber, ColorPicker, Button, Typography } from "antd";
import { HttpError, IResourceComponentsProps, useInvalidate, useTranslate } from "@refinedev/core";
import { Button, ColorPicker, Form, Input, InputNumber, Radio, Select, Typography } from "antd";
import TextArea from "antd/es/input/TextArea";
import { numberFormatter, numberParser } from "../../utils/parsing";
import { IVendor } from "../vendors/model";
import { IFilament, IFilamentParsedExtras } from "./model";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { ExtraFieldFormItem, StringifiedExtras } from "../../components/extraFields";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { useEffect, useState } from "react";
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
import { FilamentImportModal } from "../../components/filamentImportModal";
import { MultiColorPicker } from "../../components/multiColorPicker";
import { numberFormatter, numberParser } from "../../utils/parsing";
import { ExternalFilament } from "../../utils/queryExternalDB";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
import { getOrCreateVendorFromExternal } from "../vendors/functions";
import { IVendor } from "../vendors/model";
import { IFilament, IFilamentParsedExtras } from "./model";
dayjs.extend(utc);
@@ -18,15 +22,22 @@ interface CreateOrCloneProps {
mode: "create" | "clone";
}
type IFilamentRequest = Omit<IFilamentParsedExtras, "id" | "registered"> & {
vendor_id: number;
};
export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps> = (props) => {
const t = useTranslate();
const extraFields = useGetFields(EntityType.filament);
const currency = useCurrency();
const [isImportExtOpen, setIsImportExtOpen] = useState(false);
const invalidate = useInvalidate();
const [colorType, setColorType] = useState<"single" | "multi">("single");
const { form, formProps, formLoading, onFinish, redirect } = useForm<
IFilament,
HttpError,
IFilamentParsedExtras,
IFilamentRequest,
IFilamentParsedExtras
>();
@@ -39,22 +50,50 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
if (formProps.initialValues.vendor) {
formProps.initialValues.vendor_id = formProps.initialValues.vendor.id;
}
// Parse the extra fields from string values into real types
formProps.initialValues = ParsedExtras(formProps.initialValues);
}
const handleSubmit = async (redirectTo: "list" | "edit" | "create") => {
const handleSubmit = async (redirectTo: "list" | "create") => {
const values = StringifiedExtras(await form.validateFields());
await onFinish(values);
redirect(redirectTo, (values as IFilament).id);
redirect(redirectTo);
};
const { selectProps } = useSelect<IVendor>({
const { selectProps: vendorSelect } = useSelect<IVendor>({
resource: "vendor",
optionLabel: "name",
});
const importFilament = async (filament: ExternalFilament) => {
const vendor = await getOrCreateVendorFromExternal(filament.manufacturer);
await invalidate({
resource: "vendor",
invalidates: ["list", "detail"],
});
setColorType(filament.color_hexes ? "multi" : "single")
form.setFieldsValue({
name: filament.name,
vendor_id: vendor.id,
material: filament.material,
density: filament.density,
diameter: filament.diameter,
weight: filament.weight,
spool_weight: filament.spool_weight || undefined,
color_hex: filament.color_hex,
multi_color_hexes: filament.color_hexes?.join(",") || undefined,
multi_color_direction: filament.multi_color_direction,
settings_extruder_temp: filament.extruder_temp || undefined,
settings_bed_temp: filament.bed_temp || undefined,
});
};
// Use useEffect to update the form's initialValues when the extra fields are loaded
// This is necessary because the form is rendered before the extra fields are loaded
React.useEffect(() => {
useEffect(() => {
extraFields.data?.forEach((field) => {
if (formProps.initialValues && field.default_value) {
const parsedValue = JSON.parse(field.default_value as string);
@@ -67,6 +106,13 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
<Create
title={props.mode === "create" ? t("filament.titles.create") : t("filament.titles.clone")}
isLoading={formLoading}
headerButtons={() => (
<>
<Button type="primary" onClick={() => setIsImportExtOpen(true)}>
{t("filament.form.import_external")}
</Button>
</>
)}
footerButtons={() => (
<>
<Button type="primary" onClick={() => handleSubmit("list")}>
@@ -78,6 +124,14 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
</>
)}
>
<FilamentImportModal
isOpen={isImportExtOpen}
onImport={(value) => {
setIsImportExtOpen(false);
importFilament(value);
}}
onClose={() => setIsImportExtOpen(false)}
/>
<Form {...formProps} layout="vertical">
<Form.Item
label={t("filament.fields.name")}
@@ -101,7 +155,7 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
]}
>
<Select
{...selectProps}
{...vendorSelect}
allowClear
filterSort={(a, b) => {
return a?.label && b?.label
@@ -113,20 +167,62 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
}
/>
</Form.Item>
<Form.Item
label={t("filament.fields.color_hex")}
name={["color_hex"]}
rules={[
{
required: false,
},
]}
getValueFromEvent={(e) => {
return e?.toHex();
}}
>
<ColorPicker format="hex" />
<Form.Item label={t("filament.fields.color_hex")}>
<Radio.Group
onChange={(value) => {
setColorType(value.target.value);
}}
defaultValue={colorType}
value={colorType}
>
<Radio.Button value={"single"}>{t("filament.fields.single_color")}</Radio.Button>
<Radio.Button value={"multi"}>{t("filament.fields.multi_color")}</Radio.Button>
</Radio.Group>
</Form.Item>
{colorType == "single" && (
<Form.Item
name={"color_hex"}
rules={[
{
required: false,
},
]}
getValueFromEvent={(e) => {
return e?.toHex();
}}
>
<ColorPicker format="hex" />
</Form.Item>
)}
{colorType == "multi" && (
<Form.Item
name={"multi_color_direction"}
help={t("filament.fields_help.multi_color_direction")}
rules={[
{
required: true,
},
]}
initialValue={"coaxial"}
>
<Radio.Group>
<Radio.Button value={"coaxial"}>{t("filament.fields.coaxial")}</Radio.Button>
<Radio.Button value={"longitudinal"}>{t("filament.fields.longitudinal")}</Radio.Button>
</Radio.Group>
</Form.Item>
)}
{colorType == "multi" && (
<Form.Item
name={"multi_color_hexes"}
rules={[
{
required: false,
},
]}
>
<MultiColorPicker min={2} max={14} />
</Form.Item>
)}
<Form.Item
label={t("filament.fields.material")}
help={t("filament.fields_help.material")}

View File

@@ -1,16 +1,16 @@
import React, { useState } from "react";
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Edit, useForm, useSelect } from "@refinedev/antd";
import { Form, Input, DatePicker, Select, InputNumber, ColorPicker, message, Alert, Typography } from "antd";
import dayjs from "dayjs";
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Alert, ColorPicker, DatePicker, Form, Input, InputNumber, message, Radio, Select, Typography } from "antd";
import TextArea from "antd/es/input/TextArea";
import dayjs from "dayjs";
import { useEffect, useState } from "react";
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
import { MultiColorPicker } from "../../components/multiColorPicker";
import { numberFormatter, numberParser } from "../../utils/parsing";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
import { IVendor } from "../vendors/model";
import { IFilament, IFilamentParsedExtras } from "./model";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { ExtraFieldFormItem, StringifiedExtras } from "../../components/extraFields";
import { ParsedExtras } from "../../components/extraFields";
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
/*
The API returns the extra fields as JSON values, but we need to parse them into their real types
@@ -25,6 +25,7 @@ export const FilamentEdit: React.FC<IResourceComponentsProps> = () => {
const [hasChanged, setHasChanged] = useState(false);
const extraFields = useGetFields(EntityType.filament);
const currency = useCurrency();
const [colorType, setColorType] = useState<"single" | "multi">("single");
const { formProps, saveButtonProps } = useForm<IFilament, HttpError, IFilament, IFilament>({
liveMode: "manual",
@@ -49,6 +50,16 @@ export const FilamentEdit: React.FC<IResourceComponentsProps> = () => {
formProps.initialValues = ParsedExtras(formProps.initialValues);
}
// Update colorType state
useEffect(() => {
console.log(formProps.initialValues?.multi_color_hexes);
if (formProps.initialValues?.multi_color_hexes) {
setColorType("multi");
} else {
setColorType("single");
}
}, [formProps.initialValues?.multi_color_hexes]);
// Override the form's onFinish method to stringify the extra fields
const originalOnFinish = formProps.onFinish;
formProps.onFinish = (allValues: IFilamentParsedExtras) => {
@@ -132,20 +143,62 @@ export const FilamentEdit: React.FC<IResourceComponentsProps> = () => {
}
/>
</Form.Item>
<Form.Item
label={t("filament.fields.color_hex")}
name={["color_hex"]}
rules={[
{
required: false,
},
]}
getValueFromEvent={(e) => {
return e?.toHex();
}}
>
<ColorPicker format="hex" />
<Form.Item label={t("filament.fields.color_hex")}>
<Radio.Group
onChange={(value) => {
setColorType(value.target.value);
}}
defaultValue={colorType}
value={colorType}
>
<Radio.Button value={"single"}>{t("filament.fields.single_color")}</Radio.Button>
<Radio.Button value={"multi"}>{t("filament.fields.multi_color")}</Radio.Button>
</Radio.Group>
</Form.Item>
{colorType == "single" && (
<Form.Item
name={"color_hex"}
rules={[
{
required: false,
},
]}
getValueFromEvent={(e) => {
return e?.toHex();
}}
>
<ColorPicker format="hex" />
</Form.Item>
)}
{colorType == "multi" && (
<Form.Item
name={"multi_color_direction"}
help={t("filament.fields_help.multi_color_direction")}
rules={[
{
required: true,
},
]}
initialValue={"coaxial"}
>
<Radio.Group>
<Radio.Button value={"coaxial"}>{t("filament.fields.coaxial")}</Radio.Button>
<Radio.Button value={"longitudinal"}>{t("filament.fields.longitudinal")}</Radio.Button>
</Radio.Group>
</Form.Item>
)}
{colorType == "multi" && (
<Form.Item
name={"multi_color_hexes"}
rules={[
{
required: false,
},
]}
>
<MultiColorPicker min={2} max={14} />
</Form.Item>
)}
<Form.Item
label={t("filament.fields.material")}
help={t("filament.fields_help.material")}

View File

@@ -0,0 +1,50 @@
import { ExternalFilament } from "../../utils/queryExternalDB";
import { getAPIURL } from "../../utils/url";
import { getOrCreateVendorFromExternal } from "../vendors/functions";
import { IFilament } from "./model";
/**
* Create a new internal filament given an external filament object.
* Returns the created internal filament.
*/
export async function createFilamentFromExternal(externalFilament: ExternalFilament): Promise<IFilament> {
const vendor = await getOrCreateVendorFromExternal(externalFilament.manufacturer);
let color_hex: string | undefined = undefined;
let multi_color_hexes: string | undefined = undefined;
let multi_color_direction: string | undefined = undefined;
if (externalFilament.color_hex) {
color_hex = externalFilament.color_hex;
} else if (externalFilament.color_hexes && externalFilament.color_hexes.length > 0) {
multi_color_hexes = externalFilament.color_hexes.join(",")
multi_color_direction = externalFilament.multi_color_direction
}
const body: Omit<IFilament, "id" | "registered" | "extra"> & { vendor_id: number } = {
name: externalFilament.name,
material: externalFilament.material,
vendor_id: vendor.id,
density: externalFilament.density,
diameter: externalFilament.diameter,
weight: externalFilament.weight,
spool_weight: externalFilament.spool_weight || undefined,
color_hex: color_hex,
multi_color_hexes: multi_color_hexes,
multi_color_direction: multi_color_direction,
settings_extruder_temp: externalFilament.extruder_temp || undefined,
settings_bed_temp: externalFilament.bed_temp || undefined,
external_id: externalFilament.id,
};
const response = await fetch(getAPIURL() + "/filament", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json();
}

View File

@@ -1,33 +1,33 @@
import React from "react";
import { IResourceComponentsProps, useTranslate, useInvalidate, useNavigation } from "@refinedev/core";
import { useTable, List } from "@refinedev/antd";
import { Table, Button, Dropdown } from "antd";
import { EditOutlined, EyeOutlined, FileOutlined, FilterOutlined, PlusSquareOutlined } from "@ant-design/icons";
import { List, useTable } from "@refinedev/antd";
import { IResourceComponentsProps, useInvalidate, useNavigation, useTranslate } from "@refinedev/core";
import { Button, Dropdown, Table } from "antd";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { IFilament } from "./model";
import { EditOutlined, EyeOutlined, FileOutlined, FilterOutlined, PlusSquareOutlined } from "@ant-design/icons";
import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload";
import { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
ActionsColumn,
CustomFieldColumn,
DateColumn,
FilteredQueryColumn,
NumberColumn,
RichColumn,
SortedColumn,
SpoolIconColumn,
ActionsColumn,
CustomFieldColumn,
} from "../../components/column";
import { useLiveify } from "../../components/liveify";
import {
useSpoolmanArticleNumbers,
useSpoolmanFilamentNames,
useSpoolmanMaterials,
useSpoolmanVendors,
} from "../../components/otherModels";
import { useLiveify } from "../../components/liveify";
import { removeUndefined } from "../../utils/filtering";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { useNavigate } from "react-router-dom";
import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload";
import { useCurrency } from "../../utils/settings";
import { IFilament } from "./model";
dayjs.extend(utc);
@@ -125,7 +125,7 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
});
// Create state for the columns to show
const [showColumns, setShowColumns] = React.useState<string[]>(initialState.showColumns ?? defaultColumns);
const [showColumns, setShowColumns] = useState<string[]>(initialState.showColumns ?? defaultColumns);
// Store state in local storage
const tableState: TableState = {
@@ -137,7 +137,7 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
useStoreInitialState(namespace, tableState);
// Collapse the dataSource to a mutable list
const queryDataSource: IFilamentCollapsed[] = React.useMemo(
const queryDataSource: IFilamentCollapsed[] = useMemo(
() => (tableProps.dataSource || []).map((record) => ({ ...record })),
[tableProps.dataSource]
);
@@ -240,7 +240,13 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
...commonProps,
id: "name",
i18ncat: "filament",
color: (record: IFilamentCollapsed) => record.color_hex,
color: (record: IFilamentCollapsed) =>
record.multi_color_hexes
? {
colors: record.multi_color_hexes.split(","),
vertical: record.multi_color_direction === "longitudinal",
}
: record.color_hex,
filterValueQuery: useSpoolmanFilamentNames(),
}),
FilteredQueryColumn({
@@ -254,6 +260,7 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
...commonProps,
id: "price",
i18ncat: "filament",
align: "right",
width: 80,
render: (_, obj: IFilamentCollapsed) => {
return obj.price?.toLocaleString(undefined, {
@@ -285,7 +292,7 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
id: "weight",
i18ncat: "filament",
unit: "g",
maxDecimals: 1,
maxDecimals: 0,
width: 100,
}),
NumberColumn({
@@ -293,7 +300,7 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
id: "spool_weight",
i18ncat: "filament",
unit: "g",
maxDecimals: 1,
maxDecimals: 0,
width: 100,
}),
FilteredQueryColumn({
@@ -336,7 +343,7 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
i18ncat: "filament",
width: 150,
}),
ActionsColumn(actions),
ActionsColumn(t("table.actions"), actions),
])}
/>
</List>

View File

@@ -16,6 +16,9 @@ export interface IFilament {
settings_extruder_temp?: number;
settings_bed_temp?: number;
color_hex?: string;
multi_color_hexes?: string;
multi_color_direction?: string;
external_id?: string;
extra: { [key: string]: string };
}

View File

@@ -1,16 +1,17 @@
import React from "react";
import { DateField, NumberField, Show, TextField } from "@refinedev/antd";
import { IResourceComponentsProps, useShow, useTranslate } from "@refinedev/core";
import { Show, NumberField, DateField, TextField } from "@refinedev/antd";
import { Button, Typography } from "antd";
import { NumberFieldUnit } from "../../components/numberField";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { IFilament } from "./model";
import { enrichText } from "../../utils/parsing";
import React from "react";
import { useNavigate } from "react-router-dom";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { ExtraFieldDisplay } from "../../components/extraFields";
import { NumberFieldUnit } from "../../components/numberField";
import SpoolIcon from "../../components/spoolIcon";
import { enrichText } from "../../utils/parsing";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { useCurrency } from "../../utils/settings";
import { IFilament } from "./model";
dayjs.extend(utc);
const { Title } = Typography;
@@ -49,6 +50,13 @@ export const FilamentShow: React.FC<IResourceComponentsProps> = () => {
navigate(URL);
};
const colorObj = record?.multi_color_hexes
? {
colors: record.multi_color_hexes.split(","),
vertical: record.multi_color_direction === "longitudinal",
}
: record?.color_hex;
return (
<Show
isLoading={isLoading}
@@ -80,7 +88,7 @@ export const FilamentShow: React.FC<IResourceComponentsProps> = () => {
<Title level={5}>{t("filament.fields.name")}</Title>
<TextField value={record?.name} />
<Title level={5}>{t("filament.fields.color_hex")}</Title>
<TextField value={record?.color_hex} />
{colorObj && <SpoolIcon color={colorObj} size="large" />}
<Title level={5}>{t("filament.fields.material")}</Title>
<TextField value={record?.material} />
<Title level={5}>{t("filament.fields.price")}</Title>
@@ -143,6 +151,8 @@ export const FilamentShow: React.FC<IResourceComponentsProps> = () => {
)}
<Title level={5}>{t("filament.fields.article_number")}</Title>
<TextField value={record?.article_number} />
<Title level={5}>{t("filament.fields.external_id")}</Title>
<TextField value={record?.external_id} />
<Title level={5}>{t("filament.fields.comment")}</Title>
<TextField value={enrichText(record?.comment)} />
<Title level={4}>{t("settings.extra_fields.tab")}</Title>

View File

@@ -1,13 +1,13 @@
import React from "react";
import { FileOutlined, HighlightOutlined, UserOutlined } from "@ant-design/icons";
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { List, theme } from "antd";
import { Content } from "antd/es/layout/layout";
import Title from "antd/es/typography/Title";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { Content } from "antd/es/layout/layout";
import { List, theme } from "antd";
import Title from "antd/es/typography/Title";
import { FileOutlined, HighlightOutlined, UserOutlined } from "@ant-design/icons";
import { Link } from "react-router-dom";
import React from "react";
import { Trans } from "react-i18next";
import { Link } from "react-router-dom";
dayjs.extend(utc);

View File

@@ -1,14 +1,14 @@
import React, { ReactNode } from "react";
import { FileOutlined, HighlightOutlined, PlusOutlined, UnorderedListOutlined, UserOutlined } from "@ant-design/icons";
import { IResourceComponentsProps, useList, useTranslate } from "@refinedev/core";
import { Card, Col, Row, Statistic, theme } from "antd";
import { Content } from "antd/es/layout/layout";
import Title from "antd/es/typography/Title";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { Content } from "antd/es/layout/layout";
import { Card, Col, Row, Statistic, theme } from "antd";
import Title from "antd/es/typography/Title";
import Logo from "../../icon.svg?react";
import { FileOutlined, HighlightOutlined, PlusOutlined, UnorderedListOutlined, UserOutlined } from "@ant-design/icons";
import { ISpool } from "../spools/model";
import React, { ReactNode } from "react";
import { Link } from "react-router-dom";
import Logo from "../../icon.svg?react";
import { ISpool } from "../spools/model";
dayjs.extend(utc);

View File

@@ -0,0 +1,72 @@
import { PageHeader } from "@refinedev/antd";
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { theme } from "antd";
import { Content } from "antd/es/layout/layout";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import React from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import SpoolQRCodePrintingDialog from "./spoolQrCodePrintingDialog";
import SpoolSelectModal from "./spoolSelectModal";
dayjs.extend(utc);
const { useToken } = theme;
export const Printing: React.FC<IResourceComponentsProps> = () => {
const { token } = useToken();
const t = useTranslate();
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
const spoolIds = searchParams.getAll("spools").map(Number);
const step = spoolIds.length > 0 ? 1 : 0;
return (
<>
<PageHeader
title={t("printing.qrcode.button")}
onBack={() => {
const returnUrl = searchParams.get("return");
if (returnUrl) {
navigate(returnUrl, { relative: "path" });
} else {
navigate("/spool");
}
}}
>
<Content
style={{
padding: 20,
minHeight: 280,
margin: "0 auto",
backgroundColor: token.colorBgContainer,
borderRadius: token.borderRadiusLG,
color: token.colorText,
fontFamily: token.fontFamily,
fontSize: token.fontSizeLG,
lineHeight: 1.5,
}}
>
{step === 0 && (
<SpoolSelectModal
description={t("printing.spoolSelect.description")}
onContinue={(spools) => {
setSearchParams((prev) => {
const newParams = new URLSearchParams(prev);
newParams.delete("spools");
spools.forEach((spool) => newParams.append("spools", spool.id.toString()));
newParams.set("return", "/spool/print");
return newParams;
});
}}
/>
)}
{step === 1 && <SpoolQRCodePrintingDialog spoolIds={spoolIds} />}
</Content>
</PageHeader>
</>
);
};
export default Printing;

View File

@@ -0,0 +1,106 @@
import { v4 as uuidv4 } from "uuid";
import { useGetSetting, useSetSetting } from "../../utils/querySettings";
import { ISpool } from "../spools/model";
export interface PrintSettings {
id: string;
name?: string;
margin?: { top: number; bottom: number; left: number; right: number };
printerMargin?: { top: number; bottom: number; left: number; right: number };
spacing?: { horizontal: number; vertical: number };
columns?: number;
rows?: number;
skipItems?: number;
itemCopies?: number;
paperSize?: string;
customPaperSize?: { width: number; height: number };
borderShowMode?: "none" | "border" | "grid";
}
export interface QRCodePrintSettings {
showContent?: boolean;
showQRCodeMode?: "no" | "simple" | "withIcon";
textSize?: number;
printSettings: PrintSettings;
}
export interface SpoolQRCodePrintSettings {
template?: string;
labelSettings: QRCodePrintSettings;
}
export function useGetPrintSettings(): SpoolQRCodePrintSettings[] | undefined {
const { data } = useGetSetting("print_presets");
if (!data) return;
const parsed: SpoolQRCodePrintSettings[] =
data && data.value ? JSON.parse(data.value) : ([] as SpoolQRCodePrintSettings[]);
// Loop through all parsed and generate a new ID field if it's not set
return parsed.map((settings) => {
if (!settings.labelSettings.printSettings.id) {
settings.labelSettings.printSettings.id = uuidv4();
}
return settings;
});
}
export function useSetPrintSettings(): (spoolQRCodePrintSettings: SpoolQRCodePrintSettings[]) => void {
const mut = useSetSetting("print_presets");
return (spoolQRCodePrintSettings: SpoolQRCodePrintSettings[]) => {
mut.mutate(spoolQRCodePrintSettings);
};
}
interface GenericObject {
[key: string]: any;
extra: { [key: string]: string };
}
function getTagValue(tag: string, obj: GenericObject): any {
// Split tag by .
const tagParts = tag.split(".");
if (tagParts[0] === "extra") {
const extraValue = obj.extra[tagParts[1]];
if (extraValue === undefined) {
return "?";
}
return JSON.parse(extraValue);
}
const value = obj[tagParts[0]] ?? "?";
// check if value is itself an object. If so, recursively call this and remove the first part of the tag
if (typeof value === "object") {
return getTagValue(tagParts.slice(1).join("."), value);
}
return value;
}
function applyNewline(text: string): JSX.Element[] {
return text.split("\n").map((line, idx, arr) => (
<span key={idx}>
{line}
{idx < arr.length - 1 && <br />}
</span>
));
}
function applyTextFormatting(text: string): JSX.Element[] {
const regex = /\*\*([\w\W]*?)\*\*/g;
const parts = text.split(regex);
// Map over the parts and wrap matched text with <b> tags
const elements = parts.map((part, index) => {
// Even index: outside asterisks, odd index: inside asterisks (to be bolded)
const node = applyNewline(part);
return index % 2 === 0 ? <span key={index}>{node}</span> : <b key={index}>{node}</b>;
});
return elements;
}
export function renderLabelContents(template: string, spool: ISpool): JSX.Element {
// Find all {tags} in the template string and loop over them
let result = template.replace(/\{(.*?)\}/g, function (_, tag) {
return getTagValue(tag, spool);
});
// Split string on \n into individual lines
return <>{applyTextFormatting(result)}</>;
}

View File

@@ -1,29 +1,33 @@
import React, { useRef } from "react";
import { FileImageOutlined, PrinterOutlined } from "@ant-design/icons";
import { useTranslate } from "@refinedev/core";
import {
Modal,
Slider,
Button,
Select,
Row,
Col,
Form,
Divider,
RadioChangeEvent,
Radio,
InputNumber,
Collapse,
Divider,
Form,
InputNumber,
Radio,
RadioChangeEvent,
Row,
Select,
Slider,
Space,
} from "antd";
import * as htmlToImage from "html-to-image";
import React, { useRef } from "react";
import ReactToPrint from "react-to-print";
import { useSavedState } from "../../utils/saveload";
import { useTranslate } from "@refinedev/core";
import { PrintSettings } from "./printing";
interface PrintingDialogProps {
items: JSX.Element[];
printSettings: PrintSettings;
setPrintSettings: (setPrintSettings: PrintSettings) => void;
style?: string;
extraSettings?: JSX.Element;
visible: boolean;
onCancel: () => void;
title?: string;
extraSettingsStart?: JSX.Element;
extraButtons?: JSX.Element;
}
interface PaperDimensions {
@@ -58,79 +62,77 @@ const paperDimensions: { [key: string]: PaperDimensions } = {
},
};
const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSettings, visible, onCancel, title }) => {
const PrintingDialog: React.FC<PrintingDialogProps> = ({
items,
printSettings,
setPrintSettings,
style,
extraSettings,
extraSettingsStart,
extraButtons,
}) => {
const t = useTranslate();
const [collapseState, setCollapseState] = useSavedState<string[]>("print-collapseState", []);
const [marginLeft, setMarginLeft] = useSavedState("print-marginLeft", 10);
const [marginTop, setMarginTop] = useSavedState("print-marginTop", 10);
const [marginRight, setMarginRight] = useSavedState("print-marginRight", 10);
const [marginBottom, setMarginBottom] = useSavedState("print-marginBottom", 10);
const [horizontalSpacing, setHorizontalSpacing] = useSavedState("print-horizontalSpacing", 0);
const [verticalSpacing, setVerticalSpacing] = useSavedState("print-verticalSpacing", 0);
const [printerMarginLeft, setPrinterMarginLeft] = useSavedState("print-printerMarginLeft", 5);
const [printerMarginTop, setPrinterMarginTop] = useSavedState("print-printerMarginTop", 5);
const [printerMarginRight, setPrinterMarginRight] = useSavedState("print-printerMarginRight", 5);
const [printerMarginBottom, setPrinterMarginBottom] = useSavedState("print-printerMarginBottom", 5);
const [paperColumns, setPaperColumns] = useSavedState("print-itemsPerRow", 3);
const [paperRows, setPaperRows] = useSavedState("print-rowsPerPage", 8);
const [skipItems, setSkipItems] = useSavedState("print-skipItems", 0);
const [paperSize, setPaperSize] = useSavedState("print-paperSize", "A4");
const [customPaperWidth, setCustomPaperWidth] = useSavedState("print-customPaperWidth", 210);
const [customPaperHeight, setCustomPaperHeight] = useSavedState("print-customPaperHeight", 297);
const [previewScale, setPreviewScale] = useSavedState("print-previewScale", 0.6);
const [borderShowMode, setBorderShowMode] = useSavedState<"none" | "border" | "grid">("print-borderShowMode", "grid");
const paperWidth = paperSize === "custom" ? customPaperWidth : paperDimensions[paperSize].width;
const paperHeight = paperSize === "custom" ? customPaperHeight : paperDimensions[paperSize].height;
const margin = printSettings?.margin || { top: 10, bottom: 10, left: 10, right: 10 };
const printerMargin = printSettings?.printerMargin || { top: 5, bottom: 5, left: 5, right: 5 };
const spacing = printSettings?.spacing || { horizontal: 0, vertical: 0 };
const paperColumns = printSettings?.columns || 3;
const paperRows = printSettings?.rows || 8;
const skipItems = printSettings?.skipItems || 0;
const itemCopies = printSettings?.itemCopies || 1;
const paperSize = printSettings?.paperSize || "A4";
const customPaperSize = printSettings?.customPaperSize || { width: 210, height: 297 };
const borderShowMode = printSettings?.borderShowMode || "grid";
const paperWidth = paperSize === "custom" ? customPaperSize.width : paperDimensions[paperSize].width;
const paperHeight = paperSize === "custom" ? customPaperSize.height : paperDimensions[paperSize].height;
const printRef = useRef<HTMLDivElement>(null);
const itemWidth = (paperWidth - marginLeft - marginRight - horizontalSpacing) / paperColumns - horizontalSpacing;
const itemHeight = (paperHeight - marginTop - marginBottom - verticalSpacing) / paperRows - verticalSpacing;
const itemWidth = (paperWidth - margin.left - margin.right - spacing.horizontal) / paperColumns - spacing.horizontal;
const itemHeight = (paperHeight - margin.top - margin.bottom - spacing.vertical) / paperRows - spacing.vertical;
const itemsPerRow = paperColumns;
const rowsPerPage = paperRows;
const itemsPerPage = itemsPerRow * paperRows;
const itemsIncludingSkipped = [...Array(skipItems).fill(<></>), ...items];
const rowsOfItems = [];
for (let row_idx = 0; row_idx < itemsIncludingSkipped.length / itemsPerRow; row_idx += 1) {
rowsOfItems.push(itemsIncludingSkipped.slice(row_idx * itemsPerRow, (row_idx + 1) * itemsPerRow));
const itemsIncludingSkipped = [...Array(skipItems).fill(<></>)];
for (const item of items) {
for (let i = 0; i < itemCopies; i += 1) {
itemsIncludingSkipped.push(item);
}
}
const pageBlocks = [];
for (let page_idx = 0; page_idx < rowsOfItems.length / rowsPerPage; page_idx += 1) {
pageBlocks.push(rowsOfItems.slice(page_idx * rowsPerPage, (page_idx + 1) * rowsPerPage));
for (let page_idx = 0; page_idx < itemsIncludingSkipped.length / itemsPerPage; page_idx += 1) {
pageBlocks.push(itemsIncludingSkipped.slice(page_idx * itemsPerPage, (page_idx + 1) * itemsPerPage));
}
const pages = pageBlocks.map(function (rows, pageIdx) {
const itemRows = rows.map((row, rowIdx) => {
const pages = pageBlocks.map(function (items, pageIdx) {
const itemDivs = items.map((item, itemIdx) => {
const isFirstColumn = itemIdx % itemsPerRow === 0;
const isLastColumn = (itemIdx + 1) % itemsPerRow === 0;
const isFirstRow = itemIdx < itemsPerRow;
const isLastRow = itemsPerPage - itemIdx <= itemsPerRow;
return (
<tr key={rowIdx}>
{row.map(function (item, colIdx) {
return (
<td>
<div
key={colIdx}
style={{
width: `${itemWidth}mm`,
height: `${itemHeight}mm`,
border: borderShowMode === "grid" ? "1px solid #000" : "none",
paddingLeft: colIdx === 0 ? `${Math.max(printerMarginLeft - marginLeft, 0)}mm` : 0,
paddingRight:
colIdx === paperColumns - 1 ? `${Math.max(printerMarginRight - marginRight, 0)}mm` : 0,
paddingTop: rowIdx === 0 ? `${Math.max(printerMarginTop - marginTop, 0)}mm` : 0,
paddingBottom:
rowIdx === paperRows - 1 ? `${Math.max(printerMarginBottom - marginBottom, 0)}mm` : 0,
}}
>
{item}
</div>
</td>
);
})}
</tr>
<div
key={itemIdx}
className="print-page-item"
style={{
width: `${itemWidth}mm`,
height: `${itemHeight}mm`,
border: borderShowMode === "grid" ? "1px solid #000" : "none",
paddingLeft: isFirstColumn ? `${Math.max(printerMargin.left - margin.left, 0)}mm` : 0,
paddingRight: isLastColumn ? `${Math.max(printerMargin.right - margin.right, 0)}mm` : 0,
paddingTop: isFirstRow ? `${Math.max(printerMargin.top - margin.top, 0)}mm` : 0,
paddingBottom: isLastRow ? `${Math.max(printerMargin.bottom - margin.bottom, 0)}mm` : 0,
}}
>
{item}
</div>
);
});
@@ -148,57 +150,75 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
<div
className="print-page-area"
style={{
border: borderShowMode !== "none" ? "1px solid #000" : "none",
height: `${paperHeight - marginTop - marginBottom}mm`,
width: `${paperWidth - marginLeft - marginRight}mm`,
marginTop: `calc(${marginTop}mm - ${borderShowMode !== "none" ? "1px" : "0px"})`,
marginLeft: `calc(${marginLeft}mm - ${borderShowMode !== "none" ? "1px" : "0px"})`,
marginRight: `calc(${marginRight}mm - ${borderShowMode !== "none" ? "1px" : "0px"})`,
marginBottom: `calc(${marginBottom}mm - ${borderShowMode !== "none" ? "1px" : "0px"})`,
outline: borderShowMode !== "none" ? "1px solid #000" : "none",
outlineOffset: "-1px",
height: `${paperHeight - margin.top - margin.bottom}mm`,
width: `${paperWidth - margin.left - margin.right}mm`,
marginTop: `calc(${margin.top}mm - ${borderShowMode !== "none" ? "1px" : "0px"})`,
marginLeft: `calc(${margin.left}mm - ${borderShowMode !== "none" ? "1px" : "0px"})`,
marginRight: `calc(${margin.right}mm - ${borderShowMode !== "none" ? "1px" : "0px"})`,
marginBottom: `calc(${margin.bottom}mm - ${borderShowMode !== "none" ? "1px" : "0px"})`,
}}
>
<table
<div
style={{
alignContent: "flex-start",
display: "flex",
flexWrap: "wrap",
rowGap: `${spacing.vertical}mm`,
columnGap: `${spacing.horizontal}mm`,
paddingTop: `${spacing.vertical}mm`,
paddingLeft: `${spacing.horizontal}mm`,
}}
>
{itemRows}
</table>
{itemDivs}
</div>
</div>
</div>
);
});
const saveAsImage = () => {
const hasPrinted: Element[] = [];
Array.from(document.getElementsByClassName("print-qrcode-item")).forEach(async (item) => {
// Prevent printing copies
for (let i = 0; i < hasPrinted.length; i += 1) {
if (item.isEqualNode(hasPrinted[i])) return;
}
hasPrinted.push(item);
// Generate png image
const url = await htmlToImage.toPng(item as HTMLElement, {
backgroundColor: "#FFF",
cacheBust: true,
});
// Download image
const link = document.createElement("a");
link.href = url;
link.download = "spoolmanlabel.png";
link.click();
});
};
return (
<Modal
open={visible}
title={title ?? t("printing.generic.title")}
onCancel={onCancel}
footer={[
<ReactToPrint
key="print-button"
trigger={() => <Button type="primary">{t("printing.generic.print")}</Button>}
content={() => printRef.current}
/>,
]}
width={1200} // Set the modal width to accommodate the preview
>
<>
<Row gutter={16}>
<Col
span={24}
span={14}
style={{
whiteSpace: "pre-line",
marginBottom: "1em",
// This magic makes this column take the height of the sibling column
// https://stackoverflow.com/a/49065029/2911165
display: "flex",
flexDirection: "column",
}}
>
{t("printing.generic.description")}
</Col>
<Col span={14}>
<div
style={{
transform: "translateZ(0)",
overflow: "hidden scroll",
height: "70vh",
overflow: "auto",
flexBasis: "0px",
flexGrow: "1",
}}
>
<div
@@ -207,38 +227,44 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
style={{
transform: `scale(${previewScale})`,
transformOrigin: "top left",
display: "block",
}}
>
<style>
{`
@media print {
@page {
size: auto;
margin: 0;
}
.print-container {
transform: scale(1) !important;
}
.print-page {
page-break-before: auto;
}
html, body {
height: initial !important;
overflow: initial !important;
-webkit-print-color-adjust: exact;
}
@page {
size: auto;
margin: 0;
}
.print-container {
transform: scale(1) !important;
}
.print-page {
page-break-before: auto;
}
}
@media screen {
.print-page {
margin-top: 10mm;
}
.print-page {
margin-top: 10mm;
}
}
.print-page table {
border-spacing: ${horizontalSpacing}mm ${verticalSpacing}mm;
border-collapse: separate;
.print-page {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
line-height: 1.2;
}
.print-page td {
padding: 0;
.print-page * {
box-sizing: border-box;
}
${style ?? ""}
`}
</style>
@@ -248,6 +274,8 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
</Col>
<Col span={10}>
<Form labelAlign="left" colon={false} labelWrap={true} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }}>
{extraSettingsStart}
<Divider />
<Form.Item label={t("printing.generic.skipItems")}>
<Row>
<Col span={12}>
@@ -256,7 +284,8 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
max={30}
value={skipItems}
onChange={(value) => {
setSkipItems(value);
printSettings.skipItems = value;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -266,7 +295,34 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
style={{ margin: "0 16px" }}
value={skipItems}
onChange={(value) => {
setSkipItems(value ?? 1);
printSettings.skipItems = value ?? 1;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.itemCopies")}>
<Row>
<Col span={12}>
<Slider
min={1}
max={3}
value={itemCopies}
onChange={(value) => {
printSettings.itemCopies = value;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={1}
style={{ margin: "0 16px" }}
value={itemCopies}
onChange={(value) => {
printSettings.itemCopies = value ?? 1;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -283,7 +339,8 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
{ label: t("printing.generic.borders.grid"), value: "grid" },
]}
onChange={(e: RadioChangeEvent) => {
setBorderShowMode(e.target.value);
printSettings.borderShowMode = e.target.value;
setPrintSettings(printSettings);
}}
value={borderShowMode}
optionType="button"
@@ -295,7 +352,7 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
<Col span={12}>
<Slider
min={0.1}
max={1}
max={3}
step={0.01}
value={previewScale}
onChange={(value) => {
@@ -306,7 +363,7 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
<Col span={12}>
<InputNumber
min={0.1}
max={1}
max={3}
step={0.01}
style={{ margin: "0 16px" }}
value={previewScale}
@@ -332,8 +389,16 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
{extraSettings}
</Collapse.Panel>
<Collapse.Panel header={t("printing.generic.layoutSettings")} key="2">
{t("printing.generic.description")}
<Divider />
<Form.Item label={t("printing.generic.paperSize")}>
<Select value={paperSize} onChange={(value) => setPaperSize(value)}>
<Select
value={paperSize}
onChange={(value) => {
printSettings.paperSize = value;
setPrintSettings(printSettings);
}}
>
{Object.keys(paperDimensions).map((key) => (
<Select.Option key={key} value={key}>
{key}
@@ -346,10 +411,14 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
<Row align="middle">
<Col span={11}>
<InputNumber
value={customPaperWidth}
value={customPaperSize.width}
min={0.1}
addonAfter="mm"
onChange={(value) => setCustomPaperWidth(value ?? 0)}
onChange={(value) => {
customPaperSize.width = value ?? 0;
printSettings.customPaperSize = customPaperSize;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={2} style={{ textAlign: "center" }}>
@@ -357,10 +426,14 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
</Col>
<Col span={11}>
<InputNumber
value={customPaperHeight}
value={customPaperSize.height}
min={0.1}
addonAfter="mm"
onChange={(value) => setCustomPaperHeight(value ?? 0)}
onChange={(value) => {
customPaperSize.height = value ?? 0;
printSettings.customPaperSize = customPaperSize;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
@@ -373,7 +446,8 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
max={5}
value={paperColumns}
onChange={(value) => {
setPaperColumns(value);
printSettings.columns = value;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -383,7 +457,8 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
style={{ margin: "0 16px" }}
value={paperColumns}
onChange={(value) => {
setPaperColumns(value ?? 1);
printSettings.columns = value ?? 1;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -397,7 +472,8 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
max={15}
value={paperRows}
onChange={(value) => {
setPaperRows(value);
printSettings.rows = value;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -407,7 +483,8 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
style={{ margin: "0 16px" }}
value={paperRows}
onChange={(value) => {
setPaperRows(value ?? 1);
printSettings.rows = value ?? 1;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -423,9 +500,11 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={marginLeft}
value={margin.left}
onChange={(value) => {
setMarginLeft(value);
margin.left = value;
printSettings.margin = margin;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -433,10 +512,12 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={marginLeft}
value={margin.left}
addonAfter="mm"
onChange={(value) => {
setMarginLeft(value ?? 0);
margin.left = value ?? 0;
printSettings.margin = margin;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -450,9 +531,11 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={marginTop}
value={margin.top}
onChange={(value) => {
setMarginTop(value);
margin.top = value;
printSettings.margin = margin;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -460,10 +543,12 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={marginTop}
value={margin.top}
addonAfter="mm"
onChange={(value) => {
setMarginTop(value ?? 0);
margin.top = value ?? 0;
printSettings.margin = margin;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -477,9 +562,11 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={marginRight}
value={margin.right}
onChange={(value) => {
setMarginRight(value);
margin.right = value;
printSettings.margin = margin;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -487,10 +574,12 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={marginRight}
value={margin.right}
addonAfter="mm"
onChange={(value) => {
setMarginRight(value ?? 0);
margin.right = value ?? 0;
printSettings.margin = margin;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -504,9 +593,11 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={marginBottom}
value={margin.bottom}
onChange={(value) => {
setMarginBottom(value);
margin.bottom = value;
printSettings.margin = margin;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -514,10 +605,12 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={marginBottom}
value={margin.bottom}
addonAfter="mm"
onChange={(value) => {
setMarginBottom(value ?? 0);
margin.bottom = value ?? 0;
printSettings.margin = margin;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -533,9 +626,11 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={printerMarginLeft}
value={printerMargin.left}
onChange={(value) => {
setPrinterMarginLeft(value);
printerMargin.left = value;
printSettings.printerMargin = printerMargin;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -543,10 +638,12 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={printerMarginLeft}
value={printerMargin.left}
addonAfter="mm"
onChange={(value) => {
setPrinterMarginLeft(value ?? 0);
printerMargin.left = value ?? 0;
printSettings.printerMargin = printerMargin;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -560,9 +657,11 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={printerMarginTop}
value={printerMargin.top}
onChange={(value) => {
setPrinterMarginTop(value);
printerMargin.top = value;
printSettings.printerMargin = printerMargin;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -570,10 +669,12 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={printerMarginTop}
value={printerMargin.top}
addonAfter="mm"
onChange={(value) => {
setPrinterMarginTop(value ?? 0);
printerMargin.top = value ?? 0;
printSettings.printerMargin = printerMargin;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -587,9 +688,11 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={printerMarginRight}
value={printerMargin.right}
onChange={(value) => {
setPrinterMarginRight(value);
printerMargin.right = value;
printSettings.printerMargin = printerMargin;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -597,10 +700,12 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={printerMarginRight}
value={printerMargin.right}
addonAfter="mm"
onChange={(value) => {
setPrinterMarginRight(value ?? 0);
printerMargin.right = value ?? 0;
printSettings.printerMargin = printerMargin;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -614,9 +719,11 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={printerMarginBottom}
value={printerMargin.bottom}
onChange={(value) => {
setPrinterMarginBottom(value);
printerMargin.bottom = value;
printSettings.printerMargin = printerMargin;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -624,10 +731,12 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={printerMarginBottom}
value={printerMargin.bottom}
addonAfter="mm"
onChange={(value) => {
setPrinterMarginBottom(value ?? 0);
printerMargin.bottom = value ?? 0;
printSettings.printerMargin = printerMargin;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -642,9 +751,11 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
max={20}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={horizontalSpacing}
value={spacing.horizontal}
onChange={(value) => {
setHorizontalSpacing(value);
spacing.horizontal = value;
printSettings.spacing = spacing;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -652,10 +763,12 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={horizontalSpacing}
value={spacing.horizontal}
addonAfter="mm"
onChange={(value) => {
setHorizontalSpacing(value ?? 0);
spacing.horizontal = value ?? 0;
printSettings.spacing = spacing;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -669,9 +782,11 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
max={20}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={verticalSpacing}
value={spacing.vertical}
onChange={(value) => {
setVerticalSpacing(value);
spacing.vertical = value;
printSettings.spacing = spacing;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -679,10 +794,12 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={verticalSpacing}
value={spacing.vertical}
addonAfter="mm"
onChange={(value) => {
setVerticalSpacing(value ?? 0);
spacing.vertical = value ?? 0;
printSettings.spacing = spacing;
setPrintSettings(printSettings);
}}
/>
</Col>
@@ -693,7 +810,26 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSett
</Form>
</Col>
</Row>
</Modal>
<Row justify={"end"}>
<Col>
<Space>
{extraButtons}
<Button type="primary" icon={<FileImageOutlined />} size="large" onClick={saveAsImage}>
{t("printing.generic.saveAsImage")}
</Button>
<ReactToPrint
key="print-button"
trigger={() => (
<Button type="primary" icon={<PrinterOutlined />} size="large">
{t("printing.generic.print")}
</Button>
)}
content={() => printRef.current}
/>
</Space>
</Col>
</Row>
</>
);
};

View File

@@ -0,0 +1,174 @@
import { useTranslate } from "@refinedev/core";
import { Col, Form, InputNumber, QRCode, Radio, RadioChangeEvent, Row, Slider, Switch } from "antd";
import { QRCodePrintSettings } from "./printing";
import PrintingDialog from "./printingDialog";
interface QRCodeData {
value: string;
label?: JSX.Element;
errorLevel?: "L" | "M" | "Q" | "H";
}
interface QRCodePrintingDialogProps {
items: QRCodeData[];
printSettings: QRCodePrintSettings;
setPrintSettings: (setPrintSettings: QRCodePrintSettings) => void;
extraSettings?: JSX.Element;
extraSettingsStart?: JSX.Element;
extraButtons?: JSX.Element;
}
const QRCodePrintingDialog: React.FC<QRCodePrintingDialogProps> = ({
items,
printSettings,
setPrintSettings,
extraSettings,
extraSettingsStart,
extraButtons,
}) => {
const t = useTranslate();
const showContent = printSettings?.showContent === undefined ? true : printSettings?.showContent;
const showQRCodeMode = printSettings?.showQRCodeMode || "withIcon";
const textSize = printSettings?.textSize || 3;
const elements = items.map((item) => {
return (
<div className="print-qrcode-item">
{showQRCodeMode !== "no" && (
<div className="print-qrcode-container">
<QRCode
className="print-qrcode"
icon={showQRCodeMode === "withIcon" ? "/favicon.svg" : undefined}
value={item.value}
errorLevel={item.errorLevel}
type="svg"
color="#000"
/>
</div>
)}
{showContent && (
<div className="print-qrcode-title" style={showQRCodeMode === "no" ? { paddingLeft: "1mm" } : {}}>
{item.label ?? item.value}
</div>
)}
</div>
);
});
return (
<PrintingDialog
items={elements}
printSettings={printSettings.printSettings}
setPrintSettings={(newSettings) => {
printSettings.printSettings = newSettings;
setPrintSettings(printSettings);
}}
extraButtons={extraButtons}
extraSettingsStart={extraSettingsStart}
extraSettings={
<>
<Form.Item label={t("printing.qrcode.showQRCode")}>
<Radio.Group
options={[
{ label: t("printing.qrcode.showQRCodeMode.no"), value: "no" },
{
label: t("printing.qrcode.showQRCodeMode.simple"),
value: "simple",
},
{ label: t("printing.qrcode.showQRCodeMode.withIcon"), value: "withIcon" },
]}
onChange={(e: RadioChangeEvent) => {
printSettings.showQRCodeMode = e.target.value;
setPrintSettings(printSettings);
}}
value={showQRCodeMode}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
<Form.Item label={t("printing.qrcode.showContent")}>
<Switch
checked={showContent}
onChange={(checked) => {
printSettings.showContent = checked;
setPrintSettings(printSettings);
}}
/>
</Form.Item>
<Form.Item label={t("printing.qrcode.textSize")}>
<Row>
<Col span={12}>
<Slider
disabled={!showContent}
tooltip={{ formatter: (value) => `${value} mm` }}
min={2}
max={7}
value={textSize}
step={0.1}
onChange={(value) => {
printSettings.textSize = value;
setPrintSettings(printSettings);
}}
/>
</Col>
<Col span={12}>
<InputNumber
disabled={!showContent}
min={0.01}
step={0.1}
style={{ margin: "0 16px" }}
value={textSize}
addonAfter="mm"
onChange={(value) => {
printSettings.textSize = value ?? 5;
setPrintSettings(printSettings);
}}
/>
</Col>
</Row>
</Form.Item>
{extraSettings}
</>
}
style={`
.print-page .print-qrcode-item {
display: flex;
width: 100%;
height: 100%;
justify-content: center;
}
.print-page .print-qrcode-container {
max-width: ${showContent ? "50%" : "100%"};
display: flex;
}
.print-page .print-qrcode {
width: auto !important;
height: auto !important;
padding: 2mm;
}
.print-page .print-qrcode-title {
flex: 1 1 auto;
font-size: ${textSize}mm;
color: #000;
overflow: hidden;
}
.print-page canvas, .print-page svg {
/* display: block; */
object-fit: contain;
height: 100% !important;
width: 100% !important;
max-height: 100%;
max-width: 100%;
}
`}
/>
);
};
export default QRCodePrintingDialog;

View File

@@ -0,0 +1,367 @@
import { CopyOutlined, DeleteOutlined, PlusOutlined, SaveOutlined } from "@ant-design/icons";
import { useGetSetting } from "../../utils/querySettings";
import { useTranslate } from "@refinedev/core";
import { Button, Flex, Form, Input, Modal, Popconfirm, Select, Table, Typography, message, Switch } from "antd";
import TextArea from "antd/es/input/TextArea";
import { useState } from "react";
import { v4 as uuidv4 } from "uuid";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { useSavedState } from "../../utils/saveload";
import { useGetSpoolsByIds } from "../spools/functions";
import { ISpool } from "../spools/model";
import {
SpoolQRCodePrintSettings,
renderLabelContents,
useGetPrintSettings as useGetPrintPresets,
useSetPrintSettings as useSetPrintPresets,
} from "./printing";
import QRCodePrintingDialog from "./qrCodePrintingDialog";
const { Text } = Typography;
interface SpoolQRCodePrintingDialog {
spoolIds: number[];
}
const SpoolQRCodePrintingDialog: React.FC<SpoolQRCodePrintingDialog> = ({ spoolIds }) => {
const t = useTranslate();
const baseUrlSetting = useGetSetting("qr_code_url");
const baseUrlRoot =
baseUrlSetting.data?.value !== undefined && JSON.parse(baseUrlSetting.data?.value) !== ""
? JSON.parse(baseUrlSetting.data?.value)
: window.location.origin;
const [messageApi, contextHolder] = message.useMessage();
const [useHTTPUrl, setUseHTTPUrl] = useSavedState("print-useHTTPUrl", baseUrlSetting.data?.value !== "");
const itemQueries = useGetSpoolsByIds(spoolIds);
const items = itemQueries
.map((itemQuery) => {
return itemQuery.data ?? null;
})
.filter((item) => item !== null) as ISpool[];
// Selected preset state
const [selectedPresetState, setSelectedPresetState] = useSavedState<string | undefined>("selectedPreset", undefined);
// Keep a local copy of the settings which is what's actually displayed. Use the remote state only for saving.
// This decouples the debounce stuff from the UI
const [localPresets, setLocalPresets] = useState<SpoolQRCodePrintSettings[] | undefined>();
const remotePresets = useGetPrintPresets();
const setRemotePresets = useSetPrintPresets();
const localOrRemotePresets = localPresets ?? remotePresets;
const savePresetsRemote = () => {
if (!localPresets) return;
setRemotePresets(localPresets);
};
// Functions to update settings
const addNewPreset = () => {
if (!localOrRemotePresets) return;
const newId = uuidv4();
const newPreset = {
labelSettings: {
printSettings: {
id: newId,
name: t("printing.generic.newSetting"),
},
},
};
setLocalPresets([...localOrRemotePresets, newPreset]);
setSelectedPresetState(newId);
return newPreset;
};
const duplicateCurrentPreset = () => {
if (!localOrRemotePresets) return;
const newPreset = {
...curPreset,
labelSettings: { ...curPreset.labelSettings, printSettings: { ...curPreset.labelSettings.printSettings } },
};
newPreset.labelSettings.printSettings.id = uuidv4();
setLocalPresets([...localOrRemotePresets, newPreset]);
setSelectedPresetState(newPreset.labelSettings.printSettings.id);
};
const updateCurrentPreset = (newSettings: SpoolQRCodePrintSettings) => {
if (!localOrRemotePresets) return;
setLocalPresets(
localOrRemotePresets.map((presets) =>
presets.labelSettings.printSettings.id === newSettings.labelSettings.printSettings.id ? newSettings : presets
)
);
};
const deleteCurrentPreset = () => {
if (!localOrRemotePresets) return;
setLocalPresets(
localOrRemotePresets.filter((qPreset) => qPreset.labelSettings.printSettings.id !== selectedPresetState)
);
setSelectedPresetState(undefined);
};
// Initialize presets
let curPreset: SpoolQRCodePrintSettings;
if (localOrRemotePresets === undefined) {
// DB not loaded yet, use a temporary one
curPreset = {
labelSettings: {
printSettings: {
id: "TEMP",
name: t("printing.generic.newSetting"),
},
},
};
} else {
// DB is loaded, find the selected setting
if (localOrRemotePresets.length === 0) {
// DB loaded, but no settings found, add a new one and select it
const newSetting = addNewPreset();
if (!newSetting) {
console.error("Error adding new setting, this should never happen");
return;
}
// Mutate the allPrintSettings list so that the rest of the UI will work fine
localOrRemotePresets.push(newSetting);
curPreset = newSetting;
} else {
// DB loaded and at least 1 setting exists
if (!selectedPresetState) {
// No setting has been selected, select the first one
curPreset = localOrRemotePresets[0];
setSelectedPresetState(localOrRemotePresets[0].labelSettings.printSettings.id);
} else {
// A setting has been selected, find it
const foundSetting = localOrRemotePresets.find(
(settings) => settings.labelSettings.printSettings.id === selectedPresetState
);
if (foundSetting) {
curPreset = foundSetting;
} else {
// Selected setting not found, select a temp one
curPreset = {
labelSettings: {
printSettings: {
id: "TEMP",
name: t("printing.generic.newSetting"),
},
},
};
}
}
}
}
const [templateHelpOpen, setTemplateHelpOpen] = useState(false);
const template =
curPreset.template ??
`**{filament.vendor.name} - {filament.name}
#{id} - {filament.material}**
Spool Weight: {filament.spool_weight} g
ET: {filament.settings_extruder_temp} °C
BT: {filament.settings_bed_temp} °C
Lot Nr: {lot_nr}
{comment}
{filament.comment}
{filament.vendor.comment}`;
const spoolTags = [
{ tag: "id" },
{ tag: "registered" },
{ tag: "first_used" },
{ tag: "last_used" },
{ tag: "price" },
{ tag: "initial_weight" },
{ tag: "spool_weight" },
{ tag: "remaining_weight" },
{ tag: "used_weight" },
{ tag: "remaining_length" },
{ tag: "used_length" },
{ tag: "location" },
{ tag: "lot_nr" },
{ tag: "comment" },
{ tag: "archived" },
];
const spoolFields = useGetFields(EntityType.spool);
if (spoolFields.data !== undefined) {
spoolFields.data.forEach((field) => {
spoolTags.push({ tag: `extra.${field.key}` });
});
}
const filamentTags = [
{ tag: "filament.id" },
{ tag: "filament.registered" },
{ tag: "filament.name" },
{ tag: "filament.material" },
{ tag: "filament.price" },
{ tag: "filament.density" },
{ tag: "filament.diameter" },
{ tag: "filament.weight" },
{ tag: "filament.spool_weight" },
{ tag: "filament.article_number" },
{ tag: "filament.comment" },
{ tag: "filament.settings_extruder_temp" },
{ tag: "filament.settings_bed_temp" },
{ tag: "filament.color_hex" },
{ tag: "filament.multi_color_hexes" },
{ tag: "filament.multi_color_direction" },
{ tag: "filament.external_id" },
];
const filamentFields = useGetFields(EntityType.filament);
if (filamentFields.data !== undefined) {
filamentFields.data.forEach((field) => {
filamentTags.push({ tag: `filament.extra.${field.key}` });
});
}
const vendorTags = [
{ tag: "filament.vendor.id" },
{ tag: "filament.vendor.registered" },
{ tag: "filament.vendor.name" },
{ tag: "filament.vendor.comment" },
{ tag: "filament.vendor.empty_spool_weight" },
{ tag: "filament.vendor.external_id" },
];
const vendorFields = useGetFields(EntityType.vendor);
if (vendorFields.data !== undefined) {
vendorFields.data.forEach((field) => {
vendorTags.push({ tag: `filament.vendor.extra.${field.key}` });
});
}
const templateTags = [...spoolTags, ...filamentTags, ...vendorTags];
return (
<>
{contextHolder}
<QRCodePrintingDialog
printSettings={curPreset.labelSettings}
setPrintSettings={(newSettings) => {
curPreset.labelSettings = newSettings;
updateCurrentPreset(curPreset);
}}
extraSettingsStart={
<>
<Form.Item label={t("printing.generic.settings")}>
<Flex gap={8}>
<Select
value={selectedPresetState}
onChange={(value) => {
setSelectedPresetState(value);
}}
options={
localOrRemotePresets &&
localOrRemotePresets.map((settings) => ({
label: settings.labelSettings.printSettings?.name || t("printing.generic.defaultSettings"),
value: settings.labelSettings.printSettings.id,
}))
}
></Select>
<Button
style={{ width: "3em" }}
icon={<PlusOutlined />}
title={t("printing.generic.addSettings")}
onClick={addNewPreset}
/>
<Button
style={{ width: "3em" }}
icon={<CopyOutlined />}
title={t("printing.generic.duplicateSettings")}
onClick={duplicateCurrentPreset}
/>
{localOrRemotePresets && localOrRemotePresets.length > 1 && (
<Popconfirm
title={t("printing.generic.deleteSettings")}
description={t("printing.generic.deleteSettingsConfirm")}
onConfirm={deleteCurrentPreset}
okText={t("buttons.delete")}
cancelText={t("buttons.cancel")}
>
<Button
style={{ width: "3em" }}
danger
icon={<DeleteOutlined />}
title={t("printing.generic.deleteSettings")}
/>
</Popconfirm>
)}
</Flex>
</Form.Item>
<Form.Item label={t("printing.generic.settingsName")}>
<Input
value={curPreset.labelSettings.printSettings?.name}
onChange={(e) => {
curPreset.labelSettings.printSettings.name = e.target.value;
updateCurrentPreset(curPreset);
}}
/>
</Form.Item>
</>
}
items={items.map((spool) => ({
value: useHTTPUrl ? `${baseUrlRoot}/spool/show/${spool.id}` : `web+spoolman:s-${spool.id}`,
label: (
<p
style={{
padding: "1mm 1mm 1mm 0",
margin: 0,
whiteSpace: "pre-wrap",
}}
>
{renderLabelContents(template, spool)}
</p>
),
errorLevel: "H",
}))}
extraSettings={
<>
<Form.Item label={t("printing.qrcode.template")}>
<TextArea
value={template}
rows={8}
onChange={(newValue) => {
curPreset.template = newValue.target.value;
updateCurrentPreset(curPreset);
}}
/>
</Form.Item>
<Form.Item label={t("printing.qrcode.useHTTPUrl.label")} tooltip={t("printing.qrcode.useHTTPUrl.tooltip")}>
<Switch checked={useHTTPUrl} onChange={(checked) => setUseHTTPUrl(checked)} />
</Form.Item>
<Modal open={templateHelpOpen} footer={null} onCancel={() => setTemplateHelpOpen(false)}>
<Table
size="small"
showHeader={false}
pagination={false}
scroll={{ y: 400 }}
columns={[{ dataIndex: "tag" }]}
dataSource={templateTags}
/>
</Modal>
<Text type="secondary">
{t("printing.qrcode.templateHelp")}{" "}
<Button size="small" onClick={() => setTemplateHelpOpen(true)}>
{t("actions.show")}
</Button>
</Text>
</>
}
extraButtons={
<>
<Button
type="primary"
size="large"
icon={<SaveOutlined />}
onClick={() => {
savePresetsRemote();
messageApi.success(t("notifications.saveSuccessful"));
}}
>
{t("printing.generic.saveSetting")}
</Button>
</>
}
/>
</>
);
};
export default SpoolQRCodePrintingDialog;

View File

@@ -1,18 +1,17 @@
import React, { useState } from "react";
import { Modal, Table, Checkbox, Space, Row, Col, message } from "antd";
import { ISpool } from "../pages/spools/model";
import { FilteredQueryColumn, SortedColumn, SpoolIconColumn } from "./column";
import { TableState } from "../utils/saveload";
import { RightOutlined } from "@ant-design/icons";
import { useTable } from "@refinedev/antd";
import { Button, Checkbox, Col, message, Row, Space, Table } from "antd";
import { t } from "i18next";
import { useSpoolmanFilamentFilter, useSpoolmanMaterials } from "./otherModels";
import { removeUndefined } from "../utils/filtering";
import { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { FilteredQueryColumn, SortedColumn, SpoolIconColumn } from "../../components/column";
import { useSpoolmanFilamentFilter, useSpoolmanMaterials } from "../../components/otherModels";
import { removeUndefined } from "../../utils/filtering";
import { TableState } from "../../utils/saveload";
import { ISpool } from "../spools/model";
interface Props {
visible: boolean;
description?: string;
onCancel: () => void;
onContinue: (selectedSpools: ISpool[]) => void;
}
@@ -37,13 +36,14 @@ function collapseSpool(element: ISpool): ISpoolCollapsed {
};
}
const SpoolSelectModal: React.FC<Props> = ({ visible, description, onCancel, onContinue }) => {
const SpoolSelectModal: React.FC<Props> = ({ description, onContinue }) => {
const [selectedItems, setSelectedItems] = useState<number[]>([]);
const [showArchived, setShowArchived] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
const navigate = useNavigate();
const { tableProps, sorters, filters, current, pageSize } = useTable<ISpoolCollapsed>({
resource: "spool",
meta: {
queryParams: {
["allow_archived"]: showArchived,
@@ -79,7 +79,7 @@ const SpoolSelectModal: React.FC<Props> = ({ visible, description, onCancel, onC
};
// Collapse the dataSource to a mutable list and add a filament_name field
const dataSource: ISpoolCollapsed[] = React.useMemo(
const dataSource: ISpoolCollapsed[] = useMemo(
() => (tableProps.dataSource || []).map((record) => ({ ...record })),
[tableProps.dataSource]
);
@@ -116,22 +116,7 @@ const SpoolSelectModal: React.FC<Props> = ({ visible, description, onCancel, onC
};
return (
<Modal
title={t("printing.spoolSelect.title")}
open={visible}
onCancel={onCancel}
onOk={() => {
if (selectedItems.length === 0) {
messageApi.open({
type: "error",
content: t("printing.spoolSelect.noSpoolsSelected"),
});
return;
}
onContinue(dataSource.filter((spool) => selectedItems.includes(spool.id)));
}}
width={600}
>
<>
{contextHolder}
<Space direction="vertical" style={{ width: "100%" }}>
{description && <div>{description}</div>}
@@ -171,7 +156,7 @@ const SpoolSelectModal: React.FC<Props> = ({ visible, description, onCancel, onC
}),
])}
/>
<Row>
<Row gutter={[10, 10]}>
<Col span={12}>
<Checkbox
checked={isAllFilteredSelected}
@@ -208,9 +193,28 @@ const SpoolSelectModal: React.FC<Props> = ({ visible, description, onCancel, onC
{t("printing.spoolSelect.showArchived")}
</Checkbox>
</Col>
<Col span={24}>
<Button
type="primary"
icon={<RightOutlined />}
iconPosition="end"
onClick={() => {
if (selectedItems.length === 0) {
messageApi.open({
type: "error",
content: t("printing.spoolSelect.noSpoolsSelected"),
});
return;
}
onContinue(dataSource.filter((spool) => selectedItems.includes(spool.id)));
}}
>
{t("buttons.continue")}
</Button>
</Col>
</Row>
</Space>
</Modal>
</>
);
};

View File

@@ -1,3 +1,5 @@
import { PlusOutlined } from "@ant-design/icons";
import { useTranslate } from "@refinedev/core";
import {
Button,
Checkbox,
@@ -12,20 +14,18 @@ import {
Table,
message,
} from "antd";
import { EntityType, Field, FieldType, useDeleteField, useGetFields, useSetField } from "../../utils/queryFields";
import { useTranslate } from "@refinedev/core";
import { useState } from "react";
import { FormItemProps, Rule } from "antd/es/form";
import { ColumnType } from "antd/es/table";
import dayjs from "dayjs";
import advancedFormat from "dayjs/plugin/advancedFormat";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { useState } from "react";
import { Trans } from "react-i18next";
import { useParams } from "react-router-dom";
import { FormItemProps, Rule } from "antd/es/form";
import { PlusOutlined } from "@ant-design/icons";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import advancedFormat from "dayjs/plugin/advancedFormat";
import { DateTimePicker } from "../../components/dateTimePicker";
import { InputNumberRange } from "../../components/inputNumberRange";
import { EntityType, Field, FieldType, useDeleteField, useGetFields, useSetField } from "../../utils/queryFields";
dayjs.extend(utc);
dayjs.extend(timezone);

View File

@@ -1,17 +1,17 @@
import React from "react";
import { useTranslate } from "@refinedev/core";
import { Button, Form, Input, message } from "antd";
import { useEffect } from "react";
import { useGetSettings, useSetSetting } from "../../utils/querySettings";
export function GeneralSettings() {
const settings = useGetSettings();
const setSetting = useSetSetting();
const setCurrency = useSetSetting("currency");
const [form] = Form.useForm();
const [messageApi, contextHolder] = message.useMessage();
const t = useTranslate();
// Set initial form values
React.useEffect(() => {
useEffect(() => {
if (settings.data) {
form.setFieldsValue({
currency: JSON.parse(settings.data.currency.value),
@@ -21,20 +21,17 @@ export function GeneralSettings() {
}, [settings.data, form]);
// Popup message if setSetting is successful
React.useEffect(() => {
if (setSetting.isSuccess) {
useEffect(() => {
if (setCurrency.isSuccess) {
messageApi.success(t("notifications.saveSuccessful"));
}
}, [setSetting.isSuccess, messageApi, t]);
}, [setCurrency.isSuccess, messageApi, t]);
// Handle form submit
const onFinish = (values: { currency: string, qr_code_url: string }) => {
// Check if the currency has changed
if (settings.data?.currency.value !== JSON.stringify(values.currency)) {
setSetting.mutate({
key: "currency",
value: JSON.stringify(values.currency),
});
setCurrency.mutate(values.currency);
}
// Check if the QR code URL has changed
if (settings.data?.qr_code_url.value !== JSON.stringify(values.qr_code_url)) {
@@ -94,7 +91,7 @@ export function GeneralSettings() {
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit" loading={settings.isFetching || setSetting.isLoading}>
<Button type="primary" htmlType="submit" loading={settings.isFetching || setCurrency.isLoading}>
{t("buttons.save")}
</Button>
</Form.Item>

View File

@@ -1,13 +1,13 @@
import React from "react";
import { FileOutlined, HighlightOutlined, SolutionOutlined, ToolOutlined, UserOutlined } from "@ant-design/icons";
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Menu, theme } from "antd";
import { Content } from "antd/es/layout/layout";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { Content } from "antd/es/layout/layout";
import { Menu, theme } from "antd";
import { FileOutlined, HighlightOutlined, SolutionOutlined, ToolOutlined, UserOutlined } from "@ant-design/icons";
import { GeneralSettings } from "./generalSettings";
import { ExtraFieldsSettings } from "./extraFieldsSettings";
import React from "react";
import { Route, Routes, useNavigate } from "react-router-dom";
import { ExtraFieldsSettings } from "./extraFieldsSettings";
import { GeneralSettings } from "./generalSettings";
dayjs.extend(utc);

View File

@@ -1,19 +1,21 @@
import React, { useState } from "react";
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Create, useForm, useSelect } from "@refinedev/antd";
import { Form, Input, DatePicker, Select, InputNumber, Radio, Divider, Button, Typography } from "antd";
import dayjs from "dayjs";
import TextArea from "antd/es/input/TextArea";
import { IFilament } from "../filaments/model";
import { ISpool, ISpoolParsedExtras } from "./model";
import { numberFormatter, numberParser } from "../../utils/parsing";
import { useSpoolmanLocations } from "../../components/otherModels";
import { MinusOutlined, PlusOutlined } from "@ant-design/icons";
import "../../utils/overrides.css";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { ExtraFieldFormItem, StringifiedExtras } from "../../components/extraFields";
import { Create, useForm } from "@refinedev/antd";
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Alert, Button, DatePicker, Divider, Form, Input, InputNumber, Radio, Select, Typography } from "antd";
import TextArea from "antd/es/input/TextArea";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { useEffect, useMemo, useState } from "react";
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
import { useSpoolmanLocations } from "../../components/otherModels";
import { searchMatches } from "../../utils/filtering";
import "../../utils/overrides.css";
import { numberFormatter, numberParser } from "../../utils/parsing";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
import { createFilamentFromExternal } from "../filaments/functions";
import { useGetFilamentSelectOptions } from "./functions";
import { ISpool, ISpoolParsedExtras, WeightToEnter } from "./model";
dayjs.extend(utc);
@@ -21,6 +23,10 @@ interface CreateOrCloneProps {
mode: "create" | "clone";
}
type ISpoolRequest = Omit<ISpoolParsedExtras, "id" | "registered"> & {
filament_id: number | string;
};
export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps> = (props) => {
const t = useTranslate();
const extraFields = useGetFields(EntityType.spool);
@@ -29,7 +35,7 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
const { form, formProps, formLoading, onFinish, redirect } = useForm<
ISpool,
HttpError,
ISpoolParsedExtras,
ISpoolRequest,
ISpoolParsedExtras
>({
redirect: false,
@@ -39,6 +45,9 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
formProps.initialValues = {};
}
const initialWeightValue = Form.useWatch("initial_weight", form);
const spoolWeightValue = Form.useWatch("spool_weight", form);
if (props.mode === "clone") {
// Clear out the values that we don't want to clone
formProps.initialValues.first_used = null;
@@ -46,7 +55,12 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
formProps.initialValues.used_weight = 0;
// Fix the filament_id
formProps.initialValues.filament_id = formProps.initialValues.filament.id;
if (formProps.initialValues.filament) {
formProps.initialValues.filament_id = formProps.initialValues.filament.id;
}
// Parse the extra fields from string values into real types
formProps.initialValues = ParsedExtras(formProps.initialValues);
}
// If the query variable filament_id is set, set the filament_id field to that value
@@ -56,8 +70,53 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
formProps.initialValues.filament_id = parseInt(filament_id);
}
//
// Set up the filament selection options
//
const {
options: filamentOptions,
internalSelectOptions,
externalSelectOptions,
allExternalFilaments,
} = useGetFilamentSelectOptions();
const selectedFilamentID = Form.useWatch("filament_id", form);
const selectedFilament = useMemo(() => {
// id is a number of it's an internal filament, and a string of it's an external filament.
if (typeof selectedFilamentID === "number") {
return (
internalSelectOptions?.find((obj) => {
return obj.value === selectedFilamentID;
}) ?? null
);
} else if (typeof selectedFilamentID === "string") {
return (
externalSelectOptions?.find((obj) => {
return obj.value === selectedFilamentID;
}) ?? null
);
} else {
return null;
}
}, [selectedFilamentID, internalSelectOptions, externalSelectOptions]);
//
// Submit handler
//
const handleSubmit = async (redirectTo: "list" | "edit" | "create") => {
const values = StringifiedExtras(await form.validateFields());
if (selectedFilament?.is_internal === false) {
// Filament ID being a string indicates its an external filament.
// If so, we should first create the internal filament version, then create the spool(s)
const externalFilament = allExternalFilaments?.find((f) => f.id === values.filament_id);
if (!externalFilament) {
throw new Error("Unknown external filament");
}
const internalFilament = await createFilamentFromExternal(externalFilament);
values.filament_id = internalFilament.id;
}
if (quantity > 1) {
const submit = Array(quantity).fill(values);
// queue multiple creates this way for now Refine doesn't seem to map Arrays to createMany or multiple creates like it says it does
@@ -65,16 +124,13 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
} else {
await onFinish(values);
}
redirect(redirectTo, (values as ISpool).id);
};
const { queryResult } = useSelect<IFilament>({
resource: "filament",
});
redirect(redirectTo);
};
// Use useEffect to update the form's initialValues when the extra fields are loaded
// This is necessary because the form is rendered before the extra fields are loaded
React.useEffect(() => {
useEffect(() => {
extraFields.data?.forEach((field) => {
if (formProps.initialValues && field.default_value) {
const parsedValue = JSON.parse(field.default_value as string);
@@ -83,58 +139,23 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
});
}, [form, extraFields.data, formProps.initialValues]);
const filamentOptions = queryResult.data?.data.map((item) => {
let vendorPrefix = "";
if (item.vendor) {
vendorPrefix = `${item.vendor.name} - `;
}
let name = item.name;
if (!name) {
name = `ID: ${item.id}`;
}
let material = "";
if (item.material) {
material = ` - ${item.material}`;
}
const label = `${vendorPrefix}${name}${material}`;
return {
label: label,
value: item.id,
weight: item.weight,
spool_weight: item.spool_weight,
};
});
filamentOptions?.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
//
// Weight calculations
//
const [weightToEnter, setWeightToEnter] = useState(1);
const [usedWeight, setUsedWeight] = useState(0);
const selectedFilamentID = Form.useWatch("filament_id", form);
const selectedFilament = filamentOptions?.find((obj) => {
return obj.value === selectedFilamentID;
});
const filamentWeight = selectedFilament?.weight || 0;
const spoolWeight = selectedFilament?.spool_weight || 0;
const filamentChange = (newID: number) => {
const newSelectedFilament = filamentOptions?.find((obj) => {
return obj.value === newID;
});
const newFilamentWeight = newSelectedFilament?.weight || 0;
const newSpoolWeight = newSelectedFilament?.spool_weight || 0;
if (weightToEnter >= 3) {
if (!(newFilamentWeight && newSpoolWeight)) {
setWeightToEnter(2);
}
useEffect(() => {
const newFilamentWeight = selectedFilament?.weight || 0;
const newSpoolWeight = selectedFilament?.spool_weight || 0;
if (newFilamentWeight > 0) {
form.setFieldValue("initial_weight", newFilamentWeight);
}
if (weightToEnter >= 2) {
if (!newFilamentWeight) {
setWeightToEnter(1);
}
if (newSpoolWeight > 0) {
form.setFieldValue("spool_weight", newSpoolWeight);
}
};
}, [selectedFilament]);
const weightChange = (weight: number) => {
setUsedWeight(weight);
@@ -160,6 +181,67 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
setQuantity(quantity - 1);
};
const getSpoolWeight = (): number => {
return spoolWeightValue ?? selectedFilament?.spool_weight ?? 0;
};
const getFilamentWeight = (): number => {
return initialWeightValue ?? selectedFilament?.weight ?? 0;
};
const getGrossWeight = (): number => {
const net_weight = getFilamentWeight();
const spool_weight = getSpoolWeight();
return net_weight + spool_weight;
};
const getMeasuredWeight = (): number => {
const grossWeight = getGrossWeight();
return grossWeight - usedWeight;
};
const getRemainingWeight = (): number => {
const initial_weight = getFilamentWeight();
return initial_weight - usedWeight;
};
const isMeasuredWeightEnabled = (): boolean => {
if (!isRemainingWeightEnabled()) {
return false;
}
const spool_weight = spoolWeightValue;
return spool_weight || selectedFilament?.spool_weight ? true : false;
};
const isRemainingWeightEnabled = (): boolean => {
const initial_weight = initialWeightValue;
if (initial_weight) {
return true;
}
return selectedFilament?.weight ? true : false;
};
useEffect(() => {
if (weightToEnter >= WeightToEnter.measured_weight) {
if (!isMeasuredWeightEnabled()) {
setWeightToEnter(WeightToEnter.remaining_weight);
return;
}
}
if (weightToEnter >= WeightToEnter.remaining_weight) {
if (!isRemainingWeightEnabled()) {
setWeightToEnter(WeightToEnter.used_weight);
return;
}
}
}, [selectedFilament]);
return (
<Create
title={props.mode === "create" ? t("spool.titles.create") : t("spool.titles.clone")}
@@ -227,17 +309,15 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
<Select
options={filamentOptions}
showSearch
filterOption={(input, option) =>
typeof option?.label === "string" && option?.label.toLowerCase().includes(input.toLowerCase())
}
onChange={(value) => {
filamentChange(value);
}}
filterOption={(input, option) => typeof option?.label === "string" && searchMatches(input, option?.label)}
/>
</Form.Item>
{selectedFilament?.is_internal === false && (
<Alert message={t("spool.fields_help.external_filament")} type="info" />
)}
<Form.Item
label={t("filament.fields.price")}
help={t("filament.fields_help.price")}
label={t("spool.fields.price")}
help={t("spool.fields_help.price")}
name={["price"]}
rules={[
{
@@ -254,6 +334,36 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
parser={numberParser}
/>
</Form.Item>
<Form.Item
label={t("spool.fields.initial_weight")}
help={t("spool.fields_help.initial_weight")}
name={["initial_weight"]}
rules={[
{
required: false,
type: "number",
min: 0,
},
]}
>
<InputNumber addonAfter="g" precision={1} />
</Form.Item>
<Form.Item
label={t("spool.fields.spool_weight")}
help={t("spool.fields_help.spool_weight")}
name={["spool_weight"]}
rules={[
{
required: false,
type: "number",
min: 0,
},
]}
>
<InputNumber addonAfter="g" precision={1} />
</Form.Item>
<Form.Item hidden={true} name={["used_weight"]} initialValue={0}>
<InputNumber value={usedWeight} />
</Form.Item>
@@ -263,14 +373,14 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
onChange={(value) => {
setWeightToEnter(value.target.value);
}}
defaultValue={1}
defaultValue={WeightToEnter.used_weight}
value={weightToEnter}
>
<Radio.Button value={1}>{t("spool.fields.used_weight")}</Radio.Button>
<Radio.Button value={2} disabled={!filamentWeight}>
<Radio.Button value={WeightToEnter.used_weight}>{t("spool.fields.used_weight")}</Radio.Button>
<Radio.Button value={WeightToEnter.remaining_weight} disabled={!isRemainingWeightEnabled()}>
{t("spool.fields.remaining_weight")}
</Radio.Button>
<Radio.Button value={3} disabled={!(filamentWeight && spoolWeight)}>
<Radio.Button value={WeightToEnter.measured_weight} disabled={!isMeasuredWeightEnabled()}>
{t("spool.fields.measured_weight")}
</Radio.Button>
</Radio.Group>
@@ -283,7 +393,7 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
precision={1}
formatter={numberFormatter}
parser={numberParser}
disabled={weightToEnter != 1}
disabled={weightToEnter != WeightToEnter.used_weight}
value={usedWeight}
onChange={(value) => {
weightChange(value ?? 0);
@@ -301,10 +411,10 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
precision={1}
formatter={numberFormatter}
parser={numberParser}
disabled={weightToEnter != 2}
value={filamentWeight ? filamentWeight - usedWeight : 0}
disabled={weightToEnter != WeightToEnter.remaining_weight}
value={getRemainingWeight()}
onChange={(value) => {
weightChange(filamentWeight - (value ?? 0));
weightChange(getFilamentWeight() - (value ?? 0));
}}
/>
</Form.Item>
@@ -319,10 +429,11 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
precision={1}
formatter={numberFormatter}
parser={numberParser}
disabled={weightToEnter != 3}
value={filamentWeight && spoolWeight ? filamentWeight - usedWeight + spoolWeight : 0}
disabled={weightToEnter != WeightToEnter.measured_weight}
value={getMeasuredWeight()}
onChange={(value) => {
weightChange(filamentWeight - ((value ?? 0) - spoolWeight));
const totalWeight = getGrossWeight();
weightChange(totalWeight - (value ?? 0));
}}
/>
</Form.Item>

View File

@@ -1,18 +1,19 @@
import React, { useEffect, useState } from "react";
import { Edit, useForm } from "@refinedev/antd";
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Edit, useForm, useSelect } from "@refinedev/antd";
import { Form, Input, DatePicker, Select, InputNumber, Radio, Divider, Alert, Typography } from "antd";
import dayjs from "dayjs";
import { Alert, DatePicker, Divider, Form, Input, InputNumber, Radio, Select, Typography } from "antd";
import TextArea from "antd/es/input/TextArea";
import { IFilament } from "../filaments/model";
import { ISpool, ISpoolParsedExtras } from "./model";
import { numberFormatter, numberParser } from "../../utils/parsing";
import { useSpoolmanLocations } from "../../components/otherModels";
import { message } from "antd/lib";
import dayjs from "dayjs";
import { useEffect, useMemo, useState } from "react";
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
import { useSpoolmanLocations } from "../../components/otherModels";
import { searchMatches } from "../../utils/filtering";
import { numberFormatter, numberParser } from "../../utils/parsing";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { ExtraFieldFormItem, StringifiedExtras } from "../../components/extraFields";
import { ParsedExtras } from "../../components/extraFields";
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
import { createFilamentFromExternal } from "../filaments/functions";
import { useGetFilamentSelectOptions } from "./functions";
import { ISpool, ISpoolParsedExtras, WeightToEnter } from "./model";
/*
The API returns the extra fields as JSON values, but we need to parse them into their real types
@@ -21,11 +22,9 @@ We also need to stringify them again before sending them back to the API, which
the form's onFinish method. Form.Item's normalize should do this, but it doesn't seem to work.
*/
enum WeightToEnter {
used_weight = 1,
remaining_weight = 2,
measured_weight = 3,
}
type ISpoolRequest = ISpoolParsedExtras & {
filament_id: number | string;
};
export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate();
@@ -34,7 +33,7 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
const extraFields = useGetFields(EntityType.spool);
const currency = useCurrency();
const { form, formProps, saveButtonProps } = useForm<ISpool, HttpError, ISpool, ISpool>({
const { form, formProps, saveButtonProps } = useForm<ISpool, HttpError, ISpoolRequest, ISpool>({
liveMode: "manual",
onLiveEvent() {
// Warn the user if the spool has been updated since the form was opened
@@ -43,10 +42,8 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
},
});
// Get filament selection options
const { queryResult } = useSelect<IFilament>({
resource: "filament",
});
const initialWeightValue = Form.useWatch("initial_weight", form);
const spoolWeightValue = Form.useWatch("spool_weight", form);
// Add the filament_id field to the form
if (formProps.initialValues) {
@@ -56,75 +53,85 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
formProps.initialValues = ParsedExtras(formProps.initialValues);
}
//
// Set up the filament selection options
//
const {
options: filamentOptions,
internalSelectOptions,
externalSelectOptions,
allExternalFilaments,
} = useGetFilamentSelectOptions();
const selectedFilamentID = Form.useWatch("filament_id", form);
const selectedFilament = useMemo(() => {
// id is a number of it's an internal filament, and a string of it's an external filament.
if (typeof selectedFilamentID === "number") {
return (
internalSelectOptions?.find((obj) => {
return obj.value === selectedFilamentID;
}) ?? null
);
} else if (typeof selectedFilamentID === "string") {
return (
externalSelectOptions?.find((obj) => {
return obj.value === selectedFilamentID;
}) ?? null
);
} else {
return null;
}
}, [selectedFilamentID, internalSelectOptions, externalSelectOptions]);
// Override the form's onFinish method to stringify the extra fields
const originalOnFinish = formProps.onFinish;
formProps.onFinish = (allValues: ISpoolParsedExtras) => {
formProps.onFinish = (allValues: ISpoolRequest) => {
if (allValues !== undefined && allValues !== null) {
// Lot of stupidity here to make types work
const stringifiedAllValues = StringifiedExtras<ISpoolParsedExtras>(allValues);
originalOnFinish?.({
extra: {},
...stringifiedAllValues,
});
const values = StringifiedExtras<ISpoolRequest>(allValues);
if (selectedFilament?.is_internal === false) {
// Filament ID being a string indicates its an external filament.
// If so, we should first create the internal filament version, then edit the spool
const externalFilament = allExternalFilaments?.find((f) => f.id === values.filament_id);
if (!externalFilament) {
throw new Error("Unknown external filament");
}
createFilamentFromExternal(externalFilament).then((internalFilament) => {
values.filament_id = internalFilament.id;
originalOnFinish?.({
extra: {},
...values,
});
});
} else {
originalOnFinish?.({
extra: {},
...values,
});
}
}
};
const filamentOptions = queryResult.data?.data.map((item) => {
let vendorPrefix = "";
if (item.vendor) {
vendorPrefix = `${item.vendor.name} - `;
}
let name = item.name;
if (!name) {
name = `ID: ${item.id}`;
}
let material = "";
if (item.material) {
material = ` - ${item.material}`;
}
const label = `${vendorPrefix}${name}${material}`;
return {
label: label,
value: item.id,
weight: item.weight,
spool_weight: item.spool_weight,
};
});
filamentOptions?.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
const [weightToEnter, setWeightToEnter] = useState(1);
const [usedWeight, setUsedWeight] = useState(0);
const selectedFilamentID = Form.useWatch("filament_id", form);
const selectedFilament = filamentOptions?.find((obj) => {
return obj.value === selectedFilamentID;
});
const filamentWeight = selectedFilament?.weight || 0;
const spoolWeight = selectedFilament?.spool_weight || 0;
const filamentChange = (newID: number) => {
const newSelectedFilament = filamentOptions?.find((obj) => {
return obj.value === newID;
});
const filamentHasWeight = newSelectedFilament?.weight || 0;
const filamentHasSpoolWeight = newSelectedFilament?.spool_weight || 0;
if (weightToEnter == WeightToEnter.measured_weight) {
if (!(filamentHasWeight && filamentHasSpoolWeight)) {
setWeightToEnter(WeightToEnter.remaining_weight);
}
useEffect(() => {
const newFilamentWeight = selectedFilament?.weight || 0;
const newSpoolWeight = selectedFilament?.spool_weight || 0;
console.log("selectedFilament", selectedFilament, newFilamentWeight, newSpoolWeight);
if (newFilamentWeight > 0) {
form.setFieldValue("initial_weight", newFilamentWeight);
}
if (weightToEnter == WeightToEnter.remaining_weight || weightToEnter == WeightToEnter.measured_weight) {
if (!filamentHasWeight) {
setWeightToEnter(WeightToEnter.used_weight);
}
if (newSpoolWeight > 0) {
form.setFieldValue("spool_weight", newSpoolWeight);
}
};
}, [selectedFilament]);
const weightChange = (weight: number) => {
setUsedWeight(weight);
form.setFieldValue("used_weight", weight);
form.setFieldsValue({
used_weight: weight,
});
};
const locations = useSpoolmanLocations(true);
@@ -135,6 +142,67 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
allLocations.push(newLocation.trim());
}
const getSpoolWeight = (): number => {
return spoolWeightValue ?? selectedFilament?.spool_weight ?? 0;
};
const getFilamentWeight = (): number => {
return initialWeightValue ?? selectedFilament?.weight ?? 0;
};
const getGrossWeight = (): number => {
const net_weight = getFilamentWeight();
const spool_weight = getSpoolWeight();
return net_weight + spool_weight;
};
const getMeasuredWeight = (): number => {
const grossWeight = getGrossWeight();
return grossWeight - usedWeight;
};
const getRemainingWeight = (): number => {
const initial_weight = getFilamentWeight();
return initial_weight - usedWeight;
};
const isMeasuredWeightEnabled = (): boolean => {
if (!isRemainingWeightEnabled()) {
return false;
}
const spool_weight = spoolWeightValue;
return spool_weight || selectedFilament?.spool_weight ? true : false;
};
const isRemainingWeightEnabled = (): boolean => {
const initial_weight = initialWeightValue;
if (initial_weight) {
return true;
}
return selectedFilament?.weight ? true : false;
};
useEffect(() => {
if (weightToEnter >= WeightToEnter.measured_weight) {
if (!isMeasuredWeightEnabled()) {
setWeightToEnter(WeightToEnter.remaining_weight);
return;
}
}
if (weightToEnter >= WeightToEnter.remaining_weight) {
if (!isRemainingWeightEnabled()) {
setWeightToEnter(WeightToEnter.used_weight);
return;
}
}
}, [selectedFilament]);
const initialUsedWeight = formProps.initialValues?.used_weight || 0;
useEffect(() => {
if (initialUsedWeight) {
@@ -211,14 +279,12 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
<Select
options={filamentOptions}
showSearch
filterOption={(input, option) =>
typeof option?.label === "string" && option?.label.toLowerCase().includes(input.toLowerCase())
}
onChange={(value) => {
filamentChange(value);
}}
filterOption={(input, option) => typeof option?.label === "string" && searchMatches(input, option?.label)}
/>
</Form.Item>
{selectedFilament?.is_internal === false && (
<Alert message={t("spool.fields_help.external_filament")} type="info" />
)}
<Form.Item
label={t("spool.fields.price")}
help={t("spool.fields_help.price")}
@@ -238,9 +304,40 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
parser={numberParser}
/>
</Form.Item>
<Form.Item
label={t("spool.fields.initial_weight")}
help={t("spool.fields_help.initial_weight")}
name={["initial_weight"]}
rules={[
{
required: false,
type: "number",
min: 0,
},
]}
>
<InputNumber addonAfter="g" precision={1} />
</Form.Item>
<Form.Item
label={t("spool.fields.spool_weight")}
help={t("spool.fields_help.spool_weight")}
name={["spool_weight"]}
rules={[
{
required: false,
type: "number",
min: 0,
},
]}
>
<InputNumber addonAfter="g" precision={1} />
</Form.Item>
<Form.Item hidden={true} name={["used_weight"]} initialValue={0}>
<InputNumber value={usedWeight} />
</Form.Item>
<Form.Item label={t("spool.fields.weight_to_use")} help={t("spool.fields_help.weight_to_use")}>
<Radio.Group
onChange={(value) => {
@@ -250,10 +347,10 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
value={weightToEnter}
>
<Radio.Button value={WeightToEnter.used_weight}>{t("spool.fields.used_weight")}</Radio.Button>
<Radio.Button value={WeightToEnter.remaining_weight} disabled={!filamentWeight}>
<Radio.Button value={WeightToEnter.remaining_weight} disabled={!isRemainingWeightEnabled()}>
{t("spool.fields.remaining_weight")}
</Radio.Button>
<Radio.Button value={WeightToEnter.measured_weight} disabled={!(filamentWeight && spoolWeight)}>
<Radio.Button value={WeightToEnter.measured_weight} disabled={!isMeasuredWeightEnabled()}>
{t("spool.fields.measured_weight")}
</Radio.Button>
</Radio.Group>
@@ -280,9 +377,9 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
formatter={numberFormatter}
parser={numberParser}
disabled={weightToEnter != WeightToEnter.remaining_weight}
value={filamentWeight ? filamentWeight - usedWeight : 0}
value={getRemainingWeight()}
onChange={(value) => {
weightChange(filamentWeight - (value ?? 0));
weightChange(getFilamentWeight() - (value ?? 0));
}}
/>
</Form.Item>
@@ -294,9 +391,10 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
formatter={numberFormatter}
parser={numberParser}
disabled={weightToEnter != WeightToEnter.measured_weight}
value={filamentWeight && spoolWeight ? filamentWeight - usedWeight + spoolWeight : 0}
value={getMeasuredWeight()}
onChange={(value) => {
weightChange(filamentWeight - ((value ?? 0) - spoolWeight));
const totalWeight = getGrossWeight();
weightChange(totalWeight - (value ?? 0));
}}
/>
</Form.Item>

View File

@@ -1,16 +0,0 @@
import { ISpool } from "./model";
export async function setSpoolArchived(spool: ISpool, archived: boolean) {
const apiEndpoint = import.meta.env.VITE_APIURL;
const init: RequestInit = {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
archived: archived,
}),
};
const request = new Request(apiEndpoint + "/spool/" + spool.id);
await fetch(request, init);
}

View File

@@ -0,0 +1,238 @@
import { useSelect, useTranslate } from "@refinedev/core";
import { useQueries } from "@tanstack/react-query";
import { Form, InputNumber, Modal, Radio } from "antd";
import { useForm } from "antd/es/form/Form";
import { useCallback, useMemo, useState } from "react";
import { formatLength, formatWeight } from "../../utils/parsing";
import { SpoolType, useGetExternalDBFilaments } from "../../utils/queryExternalDB";
import { getAPIURL } from "../../utils/url";
import { IFilament } from "../filaments/model";
import { ISpool } from "./model";
export async function setSpoolArchived(spool: ISpool, archived: boolean) {
const init: RequestInit = {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
archived: archived,
}),
};
const request = new Request(getAPIURL() + "/spool/" + spool.id);
await fetch(request, init);
}
/**
* Use some spool filament from this spool. Either specify length or weight.
* @param spool The spool
* @param length The length to add/subtract from the spool, in mm
* @param weight The weight to add/subtract from the spool, in g
*/
export async function useSpoolFilament(spool: ISpool, length?: number, weight?: number) {
const init: RequestInit = {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
use_length: length,
use_weight: weight,
}),
};
const request = new Request(`${getAPIURL()}/spool/${spool.id}/use`);
await fetch(request, init);
}
/**
* Returns an array of queries using the useQueries hook from @tanstack/react-query.
* Each query fetches a spool by its ID from the server.
*
* @param {number[]} ids - An array of spool IDs to fetch.
* @return An array of query results, each containing the fetched spool data.
*/
export function useGetSpoolsByIds(ids: number[]) {
return useQueries({
queries: ids.map((id) => {
return {
queryKey: ["spool", id],
queryFn: async () => {
const res = await fetch(getAPIURL() + "/spool/" + id);
return (await res.json()) as ISpool;
},
};
}),
});
}
/**
* Formats a filament label with the given parameters.
*/
export function formatFilamentLabel(
name: string,
diameter: number,
vendorName?: string,
material?: string,
weight?: number,
spoolType?: SpoolType
): string {
const portions = [];
if (vendorName) {
portions.push(vendorName);
}
portions.push(name);
const extras = [];
if (material) {
extras.push(material);
}
extras.push(formatLength(diameter));
if (weight) {
extras.push(formatWeight(weight));
}
if (spoolType) {
extras.push(spoolType.charAt(0).toUpperCase() + spoolType.slice(1) + " spool");
}
return `${portions.join(" - ")} (${extras.join(", ")})`;
}
interface SelectOption {
label: string;
value: string | number;
weight?: number;
spool_weight?: number;
is_internal: boolean;
}
export function useGetFilamentSelectOptions() {
// Setup hooks
const t = useTranslate();
const { queryResult: internalFilaments } = useSelect<IFilament>({
resource: "filament",
});
const externalFilaments = useGetExternalDBFilaments();
// Format and sort internal filament options
const filamentSelectInternal: SelectOption[] = useMemo(() => {
const data =
internalFilaments.data?.data.map((item) => {
return {
label: formatFilamentLabel(
item.name ?? `ID ${item.id}`,
item.diameter,
item.vendor?.name,
item.material,
item.weight
),
value: item.id,
weight: item.weight,
spool_weight: item.spool_weight,
is_internal: true,
};
}) ?? [];
data.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
return data;
}, [internalFilaments.data?.data]);
// Format and sort external filament options
const filamentSelectExternal: SelectOption[] = useMemo(() => {
const data =
externalFilaments.data?.map((item) => {
return {
label: formatFilamentLabel(
item.name,
item.diameter,
item.manufacturer,
item.material,
item.weight,
item.spool_type
),
value: item.id,
weight: item.weight,
spool_weight: item.spool_weight || undefined,
is_internal: false,
};
}) ?? [];
data.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
return data;
}, [externalFilaments.data]);
return {
options: [
{
label: <span>{t("spool.fields.filament_internal")}</span>,
options: filamentSelectInternal,
},
{
label: <span>{t("spool.fields.filament_external")}</span>,
options: filamentSelectExternal,
},
],
internalSelectOptions: filamentSelectInternal,
externalSelectOptions: filamentSelectExternal,
allExternalFilaments: externalFilaments.data,
};
}
type MeasurementType = "length" | "weight";
export function useSpoolAdjustModal() {
const t = useTranslate();
const [form] = useForm();
const [curSpool, setCurSpool] = useState<ISpool | null>(null);
const [measurementType, setMeasurementType] = useState<MeasurementType>("length");
const openSpoolAdjustModal = useCallback((spool: ISpool) => {
setCurSpool(spool);
}, []);
const spoolAdjustModal = useMemo(() => {
if (curSpool === null) {
return null;
}
const onSubmit = async () => {
if (curSpool === null) {
return;
}
const value = form.getFieldValue("filament_value");
if (value === undefined || value === null) {
return;
}
if (measurementType === "length") {
await useSpoolFilament(curSpool, value, undefined);
} else {
await useSpoolFilament(curSpool, undefined, value);
}
setCurSpool(null);
};
return (
<Modal title={t("spool.titles.adjust")} open onCancel={() => setCurSpool(null)} onOk={form.submit}>
<p>{t("spool.form.adjust_filament_help")}</p>
<Form form={form} initialValues={{ measurement_type: measurementType }} onFinish={onSubmit}>
<Form.Item label={t("spool.form.measurement_type_label")} name="measurement_type">
<Radio.Group
value={measurementType}
onChange={({ target: { value } }) => setMeasurementType(value as MeasurementType)}
>
<Radio.Button value="length">{t("spool.form.measurement_type.length")}</Radio.Button>
<Radio.Button value="weight">{t("spool.form.measurement_type.weight")}</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label={t("spool.form.adjust_filament_value")} name="filament_value">
<InputNumber precision={1} addonAfter={measurementType === "length" ? "mm" : "g"} />
</Form.Item>
</Form>
</Modal>
);
}, [curSpool, measurementType, t]);
return {
openSpoolAdjustModal,
spoolAdjustModal,
};
}

View File

@@ -1,43 +1,44 @@
import React from "react";
import { IResourceComponentsProps, useInvalidate, useNavigation, useTranslate } from "@refinedev/core";
import { useTable, List } from "@refinedev/antd";
import { Table, Button, Dropdown, Modal } from "antd";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { ISpool } from "./model";
import { TableState, useInitialTableState, useSavedState, useStoreInitialState } from "../../utils/saveload";
import {
EditOutlined,
EyeOutlined,
FilterOutlined,
InboxOutlined,
PlusSquareOutlined,
PrinterOutlined,
ToolOutlined,
ToTopOutlined,
} from "@ant-design/icons";
import { List, useTable } from "@refinedev/antd";
import { IResourceComponentsProps, useInvalidate, useNavigation, useTranslate } from "@refinedev/core";
import { Button, Dropdown, Modal, Table } from "antd";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { useCallback, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
Action,
ActionsColumn,
CustomFieldColumn,
DateColumn,
FilteredQueryColumn,
NumberColumn,
RichColumn,
SortedColumn,
SpoolIconColumn,
Action,
ActionsColumn,
CustomFieldColumn,
} from "../../components/column";
import { setSpoolArchived } from "./functions";
import SelectAndPrint from "../../components/selectAndPrintDialog";
import { useLiveify } from "../../components/liveify";
import {
useSpoolmanFilamentFilter,
useSpoolmanLocations,
useSpoolmanLotNumbers,
useSpoolmanMaterials,
} from "../../components/otherModels";
import { useLiveify } from "../../components/liveify";
import { removeUndefined } from "../../utils/filtering";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { useNavigate } from "react-router-dom";
import { TableState, useInitialTableState, useSavedState, useStoreInitialState } from "../../utils/saveload";
import { useCurrency } from "../../utils/settings";
import { setSpoolArchived, useSpoolAdjustModal } from "./functions";
import { ISpool } from "./model";
dayjs.extend(utc);
@@ -56,7 +57,7 @@ function collapseSpool(element: ISpool): ISpoolCollapsed {
} else {
filament_name = element.filament.name ?? element.filament.id.toString();
}
if (!element.price) {
if (element.price === undefined) {
element.price = element.filament.price;
}
return {
@@ -102,6 +103,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
const navigate = useNavigate();
const extraFields = useGetFields(EntityType.spool);
const currency = useCurrency();
const { openSpoolAdjustModal, spoolAdjustModal } = useSpoolAdjustModal();
const allColumnsWithExtraFields = [...allColumns, ...(extraFields.data?.map((field) => "extra." + field.key) ?? [])];
@@ -157,7 +159,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
});
// Create state for the columns to show
const [showColumns, setShowColumns] = React.useState<string[]>(initialState.showColumns ?? defaultColumns);
const [showColumns, setShowColumns] = useState<string[]>(initialState.showColumns ?? defaultColumns);
// Store state in local storage
const tableState: TableState = {
@@ -169,7 +171,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
useStoreInitialState(namespace, tableState);
// Collapse the dataSource to a mutable list
const queryDataSource: ISpoolCollapsed[] = React.useMemo(
const queryDataSource: ISpoolCollapsed[] = useMemo(
() => (tableProps.dataSource || []).map((record) => ({ ...record })),
[tableProps.dataSource]
);
@@ -208,23 +210,27 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
}
const { editUrl, showUrl, cloneUrl } = useNavigation();
const actions = (record: ISpoolCollapsed) => {
const actions: Action[] = [
{ name: t("buttons.show"), icon: <EyeOutlined />, link: showUrl("spool", record.id) },
{ name: t("buttons.edit"), icon: <EditOutlined />, link: editUrl("spool", record.id) },
{ name: t("buttons.clone"), icon: <PlusSquareOutlined />, link: cloneUrl("spool", record.id) },
];
if (record.archived) {
actions.push({
name: t("buttons.unArchive"),
icon: <ToTopOutlined />,
onClick: () => archiveSpool(record, false),
});
} else {
actions.push({ name: t("buttons.archive"), icon: <InboxOutlined />, onClick: () => archiveSpoolPopup(record) });
}
return actions;
};
const actions = useCallback(
(record: ISpoolCollapsed) => {
const actions: Action[] = [
{ name: t("buttons.show"), icon: <EyeOutlined />, link: showUrl("spool", record.id) },
{ name: t("buttons.edit"), icon: <EditOutlined />, link: editUrl("spool", record.id) },
{ name: t("buttons.clone"), icon: <PlusSquareOutlined />, link: cloneUrl("spool", record.id) },
{ name: t("spool.titles.adjust"), icon: <ToolOutlined />, onClick: () => openSpoolAdjustModal(record) },
];
if (record.archived) {
actions.push({
name: t("buttons.unArchive"),
icon: <ToTopOutlined />,
onClick: () => archiveSpool(record, false),
});
} else {
actions.push({ name: t("buttons.archive"), icon: <InboxOutlined />, onClick: () => archiveSpoolPopup(record) });
}
return actions;
},
[t, editUrl, showUrl, cloneUrl, openSpoolAdjustModal, archiveSpool, archiveSpoolPopup]
);
const originalOnChange = tableProps.onChange;
tableProps.onChange = (pagination, filters, sorter, extra) => {
@@ -254,7 +260,15 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
<List
headerButtons={({ defaultButtons }) => (
<>
<SelectAndPrint />
<Button
type="primary"
icon={<PrinterOutlined />}
onClick={() => {
navigate("print");
}}
>
{t("printing.qrcode.button")}
</Button>
<Button
type="primary"
icon={<InboxOutlined />}
@@ -311,6 +325,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
</>
)}
>
{spoolAdjustModal}
<Table
{...tableProps}
sticky
@@ -342,7 +357,13 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
...commonProps,
id: "filament.combined_name",
i18nkey: "spool.fields.filament_name",
color: (record: ISpoolCollapsed) => record.filament.color_hex,
color: (record: ISpoolCollapsed) =>
record.filament.multi_color_hexes
? {
colors: record.filament.multi_color_hexes.split(","),
vertical: record.filament.multi_color_direction === "longitudinal",
}
: record.filament.color_hex,
dataId: "filament.combined_name",
filterValueQuery: useSpoolmanFilamentFilter(),
}),
@@ -357,6 +378,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
...commonProps,
id: "price",
i18ncat: "spool",
align: "right",
width: 80,
render: (_, obj: ISpoolCollapsed) => {
return obj.price?.toLocaleString(undefined, {
@@ -371,8 +393,9 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
...commonProps,
id: "used_weight",
i18ncat: "spool",
align: "right",
unit: "g",
maxDecimals: 1,
maxDecimals: 0,
width: 110,
}),
NumberColumn({
@@ -380,7 +403,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
id: "remaining_weight",
i18ncat: "spool",
unit: "g",
maxDecimals: 1,
maxDecimals: 0,
defaultText: t("unknown"),
width: 110,
}),
@@ -389,7 +412,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
id: "used_length",
i18ncat: "spool",
unit: "mm",
maxDecimals: 1,
maxDecimals: 0,
width: 120,
}),
NumberColumn({
@@ -397,7 +420,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
id: "remaining_length",
i18ncat: "spool",
unit: "mm",
maxDecimals: 1,
maxDecimals: 0,
defaultText: t("unknown"),
width: 120,
}),
@@ -442,7 +465,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
i18ncat: "spool",
width: 150,
}),
ActionsColumn(actions),
ActionsColumn(t("table.actions"), actions),
])}
/>
</List>

View File

@@ -1,5 +1,11 @@
import { IFilament } from "../filaments/model";
export enum WeightToEnter {
used_weight = 1,
remaining_weight = 2,
measured_weight = 3,
}
export interface ISpool {
id: number;
registered: string;
@@ -7,6 +13,8 @@ export interface ISpool {
last_used?: string;
filament: IFilament;
price?: number;
initial_weight?: number;
spool_weight?: number;
remaining_weight?: number;
used_weight: number;
remaining_length?: number;

View File

@@ -1,16 +1,18 @@
import React from "react";
import { PrinterOutlined } from "@ant-design/icons";
import { DateField, NumberField, Show, TextField } from "@refinedev/antd";
import { IResourceComponentsProps, useShow, useTranslate } from "@refinedev/core";
import { Show, NumberField, DateField, TextField } from "@refinedev/antd";
import { Typography } from "antd";
import { NumberFieldUnit } from "../../components/numberField";
import { Button, Typography } from "antd";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { ISpool } from "./model";
import { enrichText } from "../../utils/parsing";
import { IFilament } from "../filaments/model";
import { EntityType, useGetFields } from "../../utils/queryFields";
import React from "react";
import { ExtraFieldDisplay } from "../../components/extraFields";
import { NumberFieldUnit } from "../../components/numberField";
import SpoolIcon from "../../components/spoolIcon";
import { enrichText } from "../../utils/parsing";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { useCurrency } from "../../utils/settings";
import { IFilament } from "../filaments/model";
import { ISpool } from "./model";
dayjs.extend(utc);
@@ -30,7 +32,7 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
const spoolPrice = (item: ISpool) => {
let spoolPrice = "";
if (!item.price) {
if (item.price === undefined) {
spoolPrice = `${item.filament.price}`;
return spoolPrice;
}
@@ -66,12 +68,34 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
});
};
const colorObj = record?.filament.multi_color_hexes
? {
colors: record.filament.multi_color_hexes.split(","),
vertical: record.filament.multi_color_direction === "longitudinal",
}
: record?.filament.color_hex;
return (
<Show isLoading={isLoading} title={record ? formatTitle(record) : ""}>
<Show
isLoading={isLoading}
title={record ? formatTitle(record) : ""}
headerButtons={({ defaultButtons }) => (
<>
<Button
type="primary"
icon={<PrinterOutlined />}
href={"/spool/print?spools=" + record?.id + "&return=" + encodeURIComponent(window.location.pathname)}
>
{t("printing.qrcode.button")}
</Button>
{defaultButtons}
</>
)}
>
<Title level={5}>{t("spool.fields.id")}</Title>
<NumberField value={record?.id ?? ""} />
<Title level={5}>{t("spool.fields.filament")}</Title>
<TextField value={record ? filamentURL(record?.filament) : ""} />
{colorObj && <SpoolIcon color={colorObj} />} <TextField value={record ? filamentURL(record?.filament) : ""} />
<Title level={5}>{t("spool.fields.price")}</Title>
<NumberField
value={record ? spoolPrice(record) : ""}

View File

@@ -1,13 +1,13 @@
import React from "react";
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Create, useForm } from "@refinedev/antd";
import { Button, Form, Input, Typography } from "antd";
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Button, Form, Input, InputNumber, Typography } from "antd";
import TextArea from "antd/es/input/TextArea";
import { IVendor, IVendorParsedExtras } from "./model";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { ExtraFieldFormItem, StringifiedExtras } from "../../components/extraFields";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { useEffect } from "react";
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { IVendor, IVendorParsedExtras } from "./model";
dayjs.extend(utc);
@@ -30,6 +30,11 @@ export const VendorCreate: React.FC<IResourceComponentsProps & CreateOrCloneProp
formProps.initialValues = {};
}
if (props.mode === "clone") {
// Parse the extra fields from string values into real types
formProps.initialValues = ParsedExtras(formProps.initialValues);
}
const handleSubmit = async (redirectTo: "list" | "edit" | "create") => {
const values = StringifiedExtras(await form.validateFields());
await onFinish(values);
@@ -38,7 +43,7 @@ export const VendorCreate: React.FC<IResourceComponentsProps & CreateOrCloneProp
// Use useEffect to update the form's initialValues when the extra fields are loaded
// This is necessary because the form is rendered before the extra fields are loaded
React.useEffect(() => {
useEffect(() => {
extraFields.data?.forEach((field) => {
if (formProps.initialValues && field.default_value) {
const parsedValue = JSON.parse(field.default_value as string);
@@ -85,6 +90,20 @@ export const VendorCreate: React.FC<IResourceComponentsProps & CreateOrCloneProp
>
<TextArea maxLength={1024} />
</Form.Item>
<Form.Item
label={t("vendor.fields.empty_spool_weight")}
help={t("vendor.fields_help.empty_spool_weight")}
name={["empty_spool_weight"]}
rules={[
{
required: false,
type: "number",
min: 0,
},
]}
>
<InputNumber addonAfter="g" precision={1} />
</Form.Item>
<Typography.Title level={5}>{t("settings.extra_fields.tab")}</Typography.Title>
{extraFields.data?.map((field, index) => (
<ExtraFieldFormItem key={index} field={field} />

View File

@@ -1,13 +1,12 @@
import React, { useState } from "react";
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Edit, useForm } from "@refinedev/antd";
import { Form, Input, DatePicker, message, Alert, Typography } from "antd";
import dayjs from "dayjs";
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Alert, DatePicker, Form, Input, InputNumber, message, Typography } from "antd";
import TextArea from "antd/es/input/TextArea";
import { IVendor, IVendorParsedExtras } from "./model";
import dayjs from "dayjs";
import React, { useState } from "react";
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { ExtraFieldFormItem, StringifiedExtras } from "../../components/extraFields";
import { ParsedExtras } from "../../components/extraFields";
import { IVendor, IVendorParsedExtras } from "./model";
/*
The API returns the extra fields as JSON values, but we need to parse them into their real types
@@ -100,6 +99,20 @@ export const VendorEdit: React.FC<IResourceComponentsProps> = () => {
>
<TextArea maxLength={1024} />
</Form.Item>
<Form.Item
label={t("vendor.fields.empty_spool_weight")}
help={t("vendor.fields_help.empty_spool_weight")}
name={["empty_spool_weight"]}
rules={[
{
required: false,
type: "number",
min: 0,
},
]}
>
<InputNumber addonAfter="g" precision={1} />
</Form.Item>
<Typography.Title level={5}>{t("settings.extra_fields.tab")}</Typography.Title>
{extraFields.data?.map((field, index) => (
<ExtraFieldFormItem key={index} field={field} />

48
client/src/pages/vendors/functions.ts vendored Normal file
View File

@@ -0,0 +1,48 @@
import { getAPIURL } from "../../utils/url";
import { IVendor } from "./model";
/**
* Get a vendor given its external ID.
*/
export async function getVendorByExternalID(external_id: string): Promise<IVendor | null> {
// Make a search using GET and query params
const response = await fetch(`${getAPIURL()}/vendor?${new URLSearchParams({ external_id })}`);
if (!response.ok) {
return null;
}
const data: IVendor[] = await response.json();
if (data.length === 0) {
return null;
}
return data[0];
}
/**
* Create a new internal filament given an external filament object.
* Returns the created internal filament.
*/
export async function getOrCreateVendorFromExternal(vendor_external_id: string): Promise<IVendor> {
const existingVendor = await getVendorByExternalID(vendor_external_id);
if (existingVendor) {
return existingVendor;
}
const body: Omit<IVendor, "id" | "registered" | "extra"> = {
name: vendor_external_id,
external_id: vendor_external_id,
};
const response = await fetch(getAPIURL() + "/vendor", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json();
}

View File

@@ -1,23 +1,30 @@
import React from "react";
import { IResourceComponentsProps, useTranslate, useInvalidate, useNavigation } from "@refinedev/core";
import { useTable, List } from "@refinedev/antd";
import { Table, Button, Dropdown } from "antd";
import { EditOutlined, EyeOutlined, FilterOutlined, PlusSquareOutlined } from "@ant-design/icons";
import { List, useTable } from "@refinedev/antd";
import { IResourceComponentsProps, useInvalidate, useNavigation, useTranslate } from "@refinedev/core";
import { Button, Dropdown, Table } from "antd";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { IVendor } from "./model";
import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload";
import { EditOutlined, EyeOutlined, FilterOutlined, PlusSquareOutlined } from "@ant-design/icons";
import { DateColumn, RichColumn, SortedColumn, ActionsColumn, CustomFieldColumn } from "../../components/column";
import { useCallback, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
ActionsColumn,
CustomFieldColumn,
DateColumn,
NumberColumn,
RichColumn,
SortedColumn,
} from "../../components/column";
import { useLiveify } from "../../components/liveify";
import { removeUndefined } from "../../utils/filtering";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { useNavigate } from "react-router-dom";
import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload";
import { IVendor } from "./model";
dayjs.extend(utc);
const namespace = "vendorList-v2";
const allColumns: (keyof IVendor & string)[] = ["id", "name", "registered", "comment"];
const allColumns: (keyof IVendor & string)[] = ["id", "name", "registered", "comment", "empty_spool_weight"];
export const VendorList: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate();
@@ -59,7 +66,7 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
});
// Create state for the columns to show
const [showColumns, setShowColumns] = React.useState<string[]>(initialState.showColumns ?? allColumns);
const [showColumns, setShowColumns] = useState<string[]>(initialState.showColumns ?? allColumns);
// Store state in local storage
const tableState: TableState = {
@@ -71,11 +78,14 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
useStoreInitialState(namespace, tableState);
// Collapse the dataSource to a mutable list
const queryDataSource: IVendor[] = React.useMemo(
() => (tableProps.dataSource || []).map((record) => ({ ...record })),
[tableProps.dataSource]
const queryDataSource: IVendor[] = useMemo(() => {
return (tableProps.dataSource || []).map((record) => ({ ...record }));
}, [tableProps.dataSource]);
const dataSource = useLiveify(
"vendor",
queryDataSource,
useCallback((record: IVendor) => record, [])
);
const dataSource = useLiveify("vendor", queryDataSource, (record: IVendor) => record);
if (tableProps.pagination) {
tableProps.pagination.showSizeChanger = true;
@@ -171,6 +181,15 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
...commonProps,
id: "registered",
i18ncat: "vendor",
width: 200,
}),
NumberColumn({
...commonProps,
id: "empty_spool_weight",
i18ncat: "vendor",
unit: "g",
maxDecimals: 0,
width: 200,
}),
...(extraFields.data?.map((field) => {
return CustomFieldColumn({
@@ -183,7 +202,7 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
id: "comment",
i18ncat: "vendor",
}),
ActionsColumn<IVendor>(actions),
ActionsColumn<IVendor>(t("table.actions"), actions),
])}
/>
</List>

View File

@@ -3,6 +3,8 @@ export interface IVendor {
registered: string;
name: string;
comment?: string;
empty_spool_weight?: number;
external_id?: string;
extra: { [key: string]: string };
}

View File

@@ -1,13 +1,13 @@
import React from "react";
import { DateField, NumberField, Show, TextField } from "@refinedev/antd";
import { IResourceComponentsProps, useShow, useTranslate } from "@refinedev/core";
import { Show, NumberField, DateField, TextField } from "@refinedev/antd";
import { Typography } from "antd";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { IVendor } from "./model";
import React from "react";
import { ExtraFieldDisplay } from "../../components/extraFields";
import { enrichText } from "../../utils/parsing";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { ExtraFieldDisplay } from "../../components/extraFields";
import { IVendor } from "./model";
dayjs.extend(utc);
@@ -42,6 +42,10 @@ export const VendorShow: React.FC<IResourceComponentsProps> = () => {
<TextField value={record?.name} />
<Title level={5}>{t("vendor.fields.comment")}</Title>
<TextField value={enrichText(record?.comment)} />
<Title level={5}>{t("vendor.fields.empty_spool_weight")}</Title>
<TextField value={record?.empty_spool_weight} />
<Title level={5}>{t("vendor.fields.external_id")}</Title>
<TextField value={record?.external_id} />
<Title level={4}>{t("settings.extra_fields.tab")}</Title>
{extraFields?.data?.map((field, index) => (
<ExtraFieldDisplay key={index} field={field} value={record?.extra[field.key]} />

View File

@@ -35,3 +35,12 @@ export function getFiltersForField<Obj, Field extends keyof Obj>(
export function removeUndefined<T>(array: (T | undefined)[]): T[] {
return array.filter((value) => value !== undefined) as T[];
}
/**
* Performs a case-insensitive search for the given query in the given string.
* The query is broken down into words and the search is performed on each word.
*/
export function searchMatches(query: string, test: string): boolean {
const words = query.toLowerCase().split(" ");
return words.every((word) => test.toLowerCase().includes(word));
}

View File

@@ -84,3 +84,35 @@ export function enrichText(text: string | undefined) {
return <>{elements}</>;
}
/**
* Formats the weight in grams to either kilograms or grams based on the provided precision.
*
* @param {number} weightInGrams - The weight in grams to be formatted.
* @param {number} [precision=2] - The precision of the formatting (number of decimal places).
* @return {string} The formatted weight with the appropriate unit (kg or g).
*/
export function formatWeight(weightInGrams: number, precision: number = 2): string {
if (weightInGrams >= 1000) {
const kilograms = (weightInGrams / 1000).toFixed(precision).replace(/\.?0+$/, ""); // Remove trailing zeros
return `${kilograms} kg`;
} else {
return `${weightInGrams} g`;
}
}
/**
* Formats the length in millimeters to either meters or millimeters based on the provided precision.
*
* @param {number} lengthInMillimeter - The length in millimeters to be formatted.
* @param {number} [precision=2] - The precision of the formatting (number of decimal places).
* @return {string} The formatted length with the appropriate unit (m or mm).
*/
export function formatLength(lengthInMillimeter: number, precision: number = 2): string {
if (lengthInMillimeter >= 1000) {
const meters = (lengthInMillimeter / 1000).toFixed(precision).replace(/\.?0+$/, ""); // Remove trailing zeros
return `${meters} m`;
} else {
return `${lengthInMillimeter} mm`;
}
}

View File

@@ -0,0 +1,73 @@
import { useQuery } from "@tanstack/react-query";
import { getAPIURL } from "./url";
export enum SpoolType {
PLASTIC = "plastic",
CARDBOARD = "cardboard",
METAL = "metal",
}
export enum Finish {
MATTE = "matte",
GLOSSY = "glossy",
}
export enum MultiColorDirection {
COAXIAL = "coaxial",
LONGITUDINAL = "longitudinal",
}
export enum Pattern {
MARBLE = "marble",
SPARKLE = "sparkle",
}
export interface ExternalFilament {
id: string;
manufacturer: string;
name: string;
material: string;
density: number;
weight: number;
spool_weight?: number;
spool_type?: SpoolType;
diameter: number;
color_hex?: string;
color_hexes?: string[];
extruder_temp?: number;
bed_temp?: number;
finish?: Finish;
multi_color_direction?: MultiColorDirection;
pattern?: Pattern;
translucent: boolean;
glow: boolean;
}
export interface ExternalMaterial {
material: string;
density: number;
extruder_temp: number | null;
bed_temp: number | null;
}
export function useGetExternalDBFilaments() {
return useQuery<ExternalFilament[]>({
queryKey: ["external", "filaments"],
staleTime: 60,
queryFn: async () => {
const response = await fetch(`${getAPIURL()}/external/filament`);
return response.json();
},
});
}
export function useGetExternalDBMaterials() {
return useQuery<ExternalMaterial[]>({
queryKey: ["external", "materials"],
staleTime: 60,
queryFn: async () => {
const response = await fetch(`${getAPIURL()}/external/material`);
return response.json();
},
});
}

View File

@@ -1,5 +1,6 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import dayjs from "dayjs";
import { getAPIURL } from "./url";
export enum FieldType {
text = "text",
@@ -34,11 +35,10 @@ export interface Field extends FieldParameters {
}
export function useGetFields(entity_type: EntityType) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<Field[]>({
queryKey: ["fields", entity_type],
queryFn: async () => {
const response = await fetch(`${apiEndpoint}/field/${entity_type}`);
const response = await fetch(`${getAPIURL()}/field/${entity_type}`);
return response.json();
},
});
@@ -47,10 +47,9 @@ export function useGetFields(entity_type: EntityType) {
export function useSetField(entity_type: EntityType) {
const queryClient = useQueryClient();
const apiEndpoint = import.meta.env.VITE_APIURL;
return useMutation<Field[], unknown, { key: string; params: FieldParameters }, { previousFields?: Field[] }>({
mutationFn: async ({ key, params }) => {
const response = await fetch(`${apiEndpoint}/field/${entity_type}/${key}`, {
const response = await fetch(`${getAPIURL()}/field/${entity_type}/${key}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -110,10 +109,9 @@ export function useSetField(entity_type: EntityType) {
export function useDeleteField(entity_type: EntityType) {
const queryClient = useQueryClient();
const apiEndpoint = import.meta.env.VITE_APIURL;
return useMutation<Field[], unknown, string>({
mutationFn: async (key) => {
const response = await fetch(`${apiEndpoint}/field/${entity_type}/${key}`, {
const response = await fetch(`${getAPIURL()}/field/${entity_type}/${key}`, {
method: "DELETE",
});

View File

@@ -1,4 +1,5 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { getAPIURL } from "./url";
interface SettingResponseValue {
value: string;
@@ -11,39 +12,36 @@ interface SettingsResponse {
}
export function useGetSettings() {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<SettingsResponse>({
queryKey: ["settings"],
queryFn: async () => {
const response = await fetch(`${apiEndpoint}/setting`);
const response = await fetch(`${getAPIURL()}/setting/`);
return response.json();
},
});
}
export function useGetSetting(key: string) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<SettingResponseValue>({
queryKey: ["settings", key],
queryFn: async () => {
const response = await fetch(`${apiEndpoint}/setting/${key}`);
const response = await fetch(`${getAPIURL()}/setting/${key}`);
return response.json();
},
});
}
export function useSetSetting() {
export function useSetSetting<T>(key: string) {
const queryClient = useQueryClient();
const apiEndpoint = import.meta.env.VITE_APIURL;
return useMutation<SettingResponseValue, unknown, { key: string; value: unknown }>({
mutationFn: async ({ key, value }) => {
const response = await fetch(`${apiEndpoint}/setting/${key}`, {
return useMutation<SettingResponseValue, unknown, T, SettingResponseValue | undefined>({
mutationFn: async (value) => {
const response = await fetch(`${getAPIURL()}/setting/${key}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(value),
body: JSON.stringify(JSON.stringify(value)),
});
// Throw error if response is not ok
@@ -53,7 +51,18 @@ export function useSetSetting() {
return response.json();
},
onSuccess: (_data, { key }) => {
onMutate: async (value) => {
await queryClient.cancelQueries(["settings", key]);
const previousValue = queryClient.getQueryData<SettingResponseValue>(["settings", key]);
queryClient.setQueryData<SettingResponseValue>(["settings", key], (old) =>
old ? { ...old, value: JSON.stringify(value) } : undefined
);
return previousValue;
},
onError: (_error, _value, context) => {
queryClient.setQueryData<SettingResponseValue>(["settings", key], context);
},
onSuccess: (_data, _value) => {
// Invalidate and refetch
queryClient.invalidateQueries(["settings", key]);
},

View File

@@ -1,5 +1,5 @@
import React from "react";
import { CrudFilter, CrudSort } from "@refinedev/core";
import { useEffect, useState } from "react";
import { isLocalStorageAvailable } from "./support";
interface Pagination {
current: number;
@@ -14,7 +14,7 @@ export interface TableState {
}
export function useInitialTableState(tableId: string): TableState {
const [initialState] = React.useState(() => {
const [initialState] = useState(() => {
const savedSorters = hasHashProperty("sorters")
? getHashProperty("sorters")
: isLocalStorageAvailable
@@ -42,7 +42,7 @@ export function useInitialTableState(tableId: string): TableState {
}
export function useStoreInitialState(tableId: string, state: TableState) {
React.useEffect(() => {
useEffect(() => {
if (state.sorters.length > 0 && JSON.stringify(state.sorters) != JSON.stringify([{ field: "id", order: "asc" }])) {
if (isLocalStorageAvailable) {
localStorage.setItem(`${tableId}-sorters`, JSON.stringify(state.sorters));
@@ -54,7 +54,7 @@ export function useStoreInitialState(tableId: string, state: TableState) {
}
}, [tableId, state.sorters]);
React.useEffect(() => {
useEffect(() => {
const filters = state.filters.filter((f) => f.value.length != 0);
if (filters.length > 0) {
if (isLocalStorageAvailable) {
@@ -67,7 +67,7 @@ export function useStoreInitialState(tableId: string, state: TableState) {
}
}, [tableId, state.filters]);
React.useEffect(() => {
useEffect(() => {
if (JSON.stringify(state.pagination) != JSON.stringify({ current: 1, pageSize: 20 })) {
if (isLocalStorageAvailable) {
localStorage.setItem(`${tableId}-pagination`, JSON.stringify(state.pagination));
@@ -79,7 +79,7 @@ export function useStoreInitialState(tableId: string, state: TableState) {
}
}, [tableId, state.pagination]);
React.useEffect(() => {
useEffect(() => {
if (isLocalStorageAvailable) {
if (state.showColumns === undefined) {
localStorage.removeItem(`${tableId}-showColumns`);
@@ -91,12 +91,12 @@ export function useStoreInitialState(tableId: string, state: TableState) {
}
export function useSavedState<T>(id: string, defaultValue: T) {
const [state, setState] = React.useState<T>(() => {
const [state, setState] = useState<T>(() => {
const savedState = isLocalStorageAvailable ? localStorage.getItem(`savedStates-${id}`) : null;
return savedState ? JSON.parse(savedState) : defaultValue;
});
React.useEffect(() => {
useEffect(() => {
if (isLocalStorageAvailable) {
localStorage.setItem(`savedStates-${id}`, JSON.stringify(state));
}

35
client/src/utils/url.ts Normal file
View File

@@ -0,0 +1,35 @@
declare global {
interface Window {
SPOOLMAN_BASE_PATH: string;
}
}
/**
* Returns the base path of the application.
*
* If a base path is set, this returns e.g. "/spoolman". If none is set, it returns "".
*
* @return {string} The base path of the application. If the `SPOOLMAN_BASE_PATH`
* window variable is set and not empty, it is returned. Otherwise, the
* default base path "" is returned.
*/
export function getBasePath(): string {
if (window.SPOOLMAN_BASE_PATH && window.SPOOLMAN_BASE_PATH.length > 0) {
return window.SPOOLMAN_BASE_PATH;
} else {
return "";
}
}
/**
* A function that returns the Spoolman API URL
* This returns e.g. "/spoolman/api/v1" if the base path is "/spoolman"
*
* @return {string} The API URL
*/
export function getAPIURL(): string {
if (!import.meta.env.VITE_APIURL) {
throw new Error("VITE_APIURL is not set");
}
return getBasePath() + import.meta.env.VITE_APIURL;
}

Some files were not shown because too many files have changed in this diff Show More