mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-24 14:31:55 +01:00
Compare commits
344 Commits
v0.20.0
...
mk/keyless
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5131630640 | ||
|
|
b7369b00ee | ||
|
|
62ed3fabc2 | ||
|
|
304fc7f11f | ||
|
|
1b7a7a1999 | ||
|
|
a63f08ad87 | ||
|
|
9cb1a3f83c | ||
|
|
f86d38412b | ||
|
|
cbbe056d01 | ||
|
|
5f6b1a0805 | ||
|
|
27e9eb2277 | ||
|
|
6fcd10d796 | ||
|
|
377c6c6e0d | ||
|
|
7980e8e90a | ||
|
|
788d0b1c7e | ||
|
|
8b711eda99 | ||
|
|
bba0d26480 | ||
|
|
789e27e67b | ||
|
|
1828eae2c3 | ||
|
|
8c87cda9ab | ||
|
|
900604661b | ||
|
|
8af1e8fcba | ||
|
|
ed7c3dd3f5 | ||
|
|
e810571bf1 | ||
|
|
1bce1905b6 | ||
|
|
607507ad20 | ||
|
|
ed1b1a2765 | ||
|
|
5f140b34e6 | ||
|
|
3fbf154589 | ||
|
|
2bfd612971 | ||
|
|
fe37c5acc7 | ||
|
|
6be9c18f68 | ||
|
|
7d5d4e7dc7 | ||
|
|
ec7051672f | ||
|
|
008725b300 | ||
|
|
3fb828ee1a | ||
|
|
0adebeaf8d | ||
|
|
c1a944411c | ||
|
|
1aaab56045 | ||
|
|
87ecb217fb | ||
|
|
91e4df652d | ||
|
|
40ee154508 | ||
|
|
1925167407 | ||
|
|
b8bdf23d05 | ||
|
|
ca49a4cd82 | ||
|
|
c8c1a4f573 | ||
|
|
9f5fb82c47 | ||
|
|
d87c46a464 | ||
|
|
7e5567bd2f | ||
|
|
5589301c9d | ||
|
|
b489593e62 | ||
|
|
38413ddef4 | ||
|
|
273520fd96 | ||
|
|
4704b42b6d | ||
|
|
29c84e3071 | ||
|
|
6d3967383e | ||
|
|
c7af7720ea | ||
|
|
44ea3aef1b | ||
|
|
414599503f | ||
|
|
5eda237014 | ||
|
|
6e2b0f2d32 | ||
|
|
2fc9d40419 | ||
|
|
5ed5d69d34 | ||
|
|
19605bc242 | ||
|
|
523c3af677 | ||
|
|
c2d64388b2 | ||
|
|
2c8bc77aaa | ||
|
|
284e38c92c | ||
|
|
85fc35a382 | ||
|
|
9ffe8ec399 | ||
|
|
1e4902d8ae | ||
|
|
6585a271f6 | ||
|
|
faa9e09efe | ||
|
|
55b73418b8 | ||
|
|
8be61d9e36 | ||
|
|
174286b701 | ||
|
|
385baf1068 | ||
|
|
25104465ca | ||
|
|
dbdc9f6531 | ||
|
|
2fe3cd9041 | ||
|
|
9c8a9d32b6 | ||
|
|
4b68162b1d | ||
|
|
3fa0ff5214 | ||
|
|
59c2074343 | ||
|
|
2c7d7b9d53 | ||
|
|
741baeb7fb | ||
|
|
65c1d20f17 | ||
|
|
23eec20e97 | ||
|
|
e9e0ccca99 | ||
|
|
00a1efce1d | ||
|
|
de7345f326 | ||
|
|
10564bfc9f | ||
|
|
508c5ee116 | ||
|
|
0dfc634d1b | ||
|
|
e92eb80aec | ||
|
|
5d84cc2899 | ||
|
|
19db9f5623 | ||
|
|
0f163e48e2 | ||
|
|
fb6df194d5 | ||
|
|
762a309e4b | ||
|
|
cf7f703f69 | ||
|
|
0e71f59086 | ||
|
|
b0829b7f4d | ||
|
|
305207fcd7 | ||
|
|
6deda72650 | ||
|
|
e8e6d6e81b | ||
|
|
1e06a6e4e0 | ||
|
|
064c945d9c | ||
|
|
8814d63655 | ||
|
|
4954b79cbd | ||
|
|
6fa331307a | ||
|
|
1a95ff4854 | ||
|
|
c77f2eb119 | ||
|
|
79b04203b9 | ||
|
|
32258535a5 | ||
|
|
4fb61bc4a5 | ||
|
|
55fed18582 | ||
|
|
408391d31f | ||
|
|
0087d810ae | ||
|
|
be907f72ff | ||
|
|
669543989a | ||
|
|
484744c0f9 | ||
|
|
912a11f27d | ||
|
|
a49e6e4f92 | ||
|
|
f94167cb34 | ||
|
|
4aa6f12df4 | ||
|
|
2ac5c08f76 | ||
|
|
49f891f577 | ||
|
|
25cf4ecc51 | ||
|
|
e77f1dd68c | ||
|
|
4cfece1bf5 | ||
|
|
6e5b348d82 | ||
|
|
d53c643de0 | ||
|
|
8c53d76819 | ||
|
|
5364833afb | ||
|
|
541585c0bb | ||
|
|
350a35f7f4 | ||
|
|
856f2584b9 | ||
|
|
c997f274cc | ||
|
|
e9689b6b52 | ||
|
|
3713816576 | ||
|
|
3529a95ebe | ||
|
|
fa066bc962 | ||
|
|
ba358790ea | ||
|
|
3aff39cdaf | ||
|
|
877bb2ddbf | ||
|
|
c8a48e4400 | ||
|
|
1211105eb4 | ||
|
|
28ce0d29a4 | ||
|
|
dbf8322ec6 | ||
|
|
9f34f80a60 | ||
|
|
175b93a62e | ||
|
|
d41f313cff | ||
|
|
1439e20d93 | ||
|
|
17e3a6d0cf | ||
|
|
1ed7734b2e | ||
|
|
362c0bb3e6 | ||
|
|
0d3151ae5c | ||
|
|
b4e679e321 | ||
|
|
de3b63639b | ||
|
|
23ba40892a | ||
|
|
624c1763ac | ||
|
|
75c2423fd5 | ||
|
|
d4f2b52b6c | ||
|
|
028b1382ad | ||
|
|
d8781950fa | ||
|
|
8646360b8c | ||
|
|
6ce83ea04c | ||
|
|
ad356acc73 | ||
|
|
863b84355d | ||
|
|
959d9961f1 | ||
|
|
c5b783bef7 | ||
|
|
1d78b953dd | ||
|
|
44f5aaec57 | ||
|
|
4933446202 | ||
|
|
e1fbb99203 | ||
|
|
4a9557fcb7 | ||
|
|
5766277c16 | ||
|
|
5374f31d69 | ||
|
|
e82f5084d4 | ||
|
|
bbd773fb3a | ||
|
|
7129650efa | ||
|
|
a57b83c52d | ||
|
|
bb5e36f0c4 | ||
|
|
bd44b36666 | ||
|
|
895063fa36 | ||
|
|
aa7658b0d4 | ||
|
|
68f97f24c7 | ||
|
|
6555c9277a | ||
|
|
b5d13380fe | ||
|
|
9271cdae4b | ||
|
|
18149a5c9a | ||
|
|
68b6d58ab4 | ||
|
|
6d516f6de6 | ||
|
|
36d5ae1466 | ||
|
|
f37f609dff | ||
|
|
a980d9f243 | ||
|
|
aac82c9236 | ||
|
|
8dedfcca43 | ||
|
|
f72fcb0800 | ||
|
|
94e81809d3 | ||
|
|
e80e5744f7 | ||
|
|
402b8c429e | ||
|
|
d2919de8e8 | ||
|
|
8a60729153 | ||
|
|
4a4bf9a175 | ||
|
|
24923f2a83 | ||
|
|
66c2de22ed | ||
|
|
c93fddae7f | ||
|
|
fb17b56f09 | ||
|
|
a3c13a8a74 | ||
|
|
09f29d82f4 | ||
|
|
dd94fd43ee | ||
|
|
a85bdfef88 | ||
|
|
79baf6b5ef | ||
|
|
d691e908a4 | ||
|
|
ec8320bc42 | ||
|
|
6dbb243ba5 | ||
|
|
7c56bfb4ab | ||
|
|
c3af4ac4ac | ||
|
|
fc88df0ff0 | ||
|
|
0e1e5ae3f0 | ||
|
|
0ed69b75a1 | ||
|
|
c666a8a8c1 | ||
|
|
6ef7045f62 | ||
|
|
98ce90636d | ||
|
|
86721c9b9a | ||
|
|
62f6121260 | ||
|
|
90bb6ed1fe | ||
|
|
bd79ee3227 | ||
|
|
c0e79cdb9e | ||
|
|
5156792319 | ||
|
|
8bbc39e416 | ||
|
|
0beb430704 | ||
|
|
0f7107f86d | ||
|
|
115cda5c37 | ||
|
|
a6c1c8c652 | ||
|
|
c69c6a1518 | ||
|
|
adaffa5ca8 | ||
|
|
b410642dc6 | ||
|
|
4bed1a3158 | ||
|
|
9ff39bb402 | ||
|
|
3ab250a045 | ||
|
|
4147cff1db | ||
|
|
dada2f0266 | ||
|
|
e9e852c8a3 | ||
|
|
7dda0f473a | ||
|
|
2006b8056a | ||
|
|
41f63456eb | ||
|
|
fe177deff4 | ||
|
|
d729a74b34 | ||
|
|
6ab51e4767 | ||
|
|
e080817e1a | ||
|
|
31e6f0264d | ||
|
|
8e98ded03f | ||
|
|
8da030d415 | ||
|
|
393342bc32 | ||
|
|
9f331b87df | ||
|
|
27efa00ee2 | ||
|
|
1224a6e516 | ||
|
|
988f9eee8c | ||
|
|
832b4a6484 | ||
|
|
64298511ee | ||
|
|
f4ed929e4a | ||
|
|
b272c97694 | ||
|
|
3004d376ab | ||
|
|
8f440e2a64 | ||
|
|
017b05452a | ||
|
|
6a1f2549df | ||
|
|
2f51ba419b | ||
|
|
bcd77ee796 | ||
|
|
23cecfb2a5 | ||
|
|
f4c8dd5450 | ||
|
|
72033341b4 | ||
|
|
c2cfa10336 | ||
|
|
850ed476d4 | ||
|
|
adea83d421 | ||
|
|
d678c35c57 | ||
|
|
d3073b472d | ||
|
|
b274f81dbb | ||
|
|
721e407600 | ||
|
|
ca4aed7bd3 | ||
|
|
746bd50f24 | ||
|
|
945a768691 | ||
|
|
27237ae6d3 | ||
|
|
4463867cf0 | ||
|
|
95e2fb6a15 | ||
|
|
e32dd0aaa5 | ||
|
|
ee5c43dc29 | ||
|
|
17c9685391 | ||
|
|
fd41065250 | ||
|
|
f9b1327507 | ||
|
|
5ed0e5c000 | ||
|
|
ce1e58828a | ||
|
|
d74508e214 | ||
|
|
178e676521 | ||
|
|
c215373458 | ||
|
|
82bceb2185 | ||
|
|
be1f3c0ad3 | ||
|
|
518d13ccbb | ||
|
|
8bef7b236b | ||
|
|
e774e57bee | ||
|
|
d6d0d6dc56 | ||
|
|
7e0ea5fee5 | ||
|
|
10dcc1c01d | ||
|
|
38c37111cf | ||
|
|
bf27d147dd | ||
|
|
000ccd6d38 | ||
|
|
f6fc30e218 | ||
|
|
b444774f9b | ||
|
|
91373dceb8 | ||
|
|
ceb1bf89a1 | ||
|
|
eb1428d3ac | ||
|
|
e4ac7633a5 | ||
|
|
bd604f5867 | ||
|
|
b6b939db40 | ||
|
|
d3f56b1b95 | ||
|
|
a258e1d2bc | ||
|
|
5ca671a4ab | ||
|
|
ccdab8bac1 | ||
|
|
0d2a6d6ac8 | ||
|
|
e159dd8a0b | ||
|
|
b311a5c9ed | ||
|
|
04c8e38ecf | ||
|
|
1fd2f42282 | ||
|
|
d3cff18cc6 | ||
|
|
3e27c24fbd | ||
|
|
acd5acd4cf | ||
|
|
71dc5fcb23 | ||
|
|
2ff5f4ca0b | ||
|
|
12831a40d0 | ||
|
|
c966090889 | ||
|
|
f03eb637a7 | ||
|
|
49ea34f352 | ||
|
|
85d91667eb | ||
|
|
458554b6e1 | ||
|
|
79ff5cedc6 | ||
|
|
f0008abd04 | ||
|
|
8a377b3e4d | ||
|
|
e7d31722f7 | ||
|
|
52b4506e12 | ||
|
|
cb9631c999 | ||
|
|
6e3186a9de | ||
|
|
a67070f965 |
@@ -29,6 +29,6 @@
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "node",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/go:1": "1.21"
|
||||
"ghcr.io/devcontainers/features/go:1": "1.24"
|
||||
}
|
||||
}
|
||||
|
||||
40
.github/AGENTS.md
vendored
Normal file
40
.github/AGENTS.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
This is a Go based repository with a VueJS client for the frontend built with Vite and Nuxt, with ShadCN.
|
||||
|
||||
To make life easier, the use of a Taskfile is included for the majority of development commands.
|
||||
|
||||
Please follow these guidelines when contributing:
|
||||
|
||||
## Required Before Each Commit
|
||||
- Generate Swagger Files: `task swag --force`
|
||||
- Generate JS API Client: `task typescript-types --force`
|
||||
- Lint Golang: `task go:lint`
|
||||
- Lint frontend: `task ui:fix`
|
||||
|
||||
## Repository Structure
|
||||
### Backend
|
||||
- `backend/`: Contains the backend folders
|
||||
- `backend/app`: Contains main app code including API endpoints
|
||||
- `backend/internal/core`: Contains basic services such as currencies
|
||||
- `backend/data`: Contains all information related to data, including `ent` schemas, repos, migrations, etc.
|
||||
- `backend/data/migrations`: Contains migration data, the `sqlite3` sub-folder contains sqlite migrations, `postgres` sub-folder the postgres migrations, BOTH are REQUIRED.
|
||||
- `backend/data/ent/schema`: Contains the actual `ent` data models.
|
||||
- `backend/data/repo`: Contains the data repositories
|
||||
- `backend/pkgs`: Contains general helper functions and services
|
||||
|
||||
### Frontend
|
||||
- `frontend/`: Contains initial frontend files
|
||||
- `frontend/components`: Contains the ShadCN components
|
||||
- `frontend/locales`: Contains the i18n JSON for languages
|
||||
- `frontend/pages`: Contains VueJS pages
|
||||
- `frontend/test`: Contains Playwright setup
|
||||
- `frontend/test/e2e`: Contains actual Playwright test files
|
||||
|
||||
### Docs
|
||||
- `docs/`: Contains VitePress based documentation
|
||||
|
||||
## Key Guidelines
|
||||
1. Follow best practices for the various programming languages
|
||||
2. Maintain existing code structure and organization when possible
|
||||
3. Use dependency injection when reasonable
|
||||
4. Write tests for new functionality and after fixing bugs to validate they're fixed
|
||||
5. Document changes to the `docs/` folder when appropriate
|
||||
21
.github/workflows/binaries-publish.yaml
vendored
21
.github/workflows/binaries-publish.yaml
vendored
@@ -1,6 +1,7 @@
|
||||
name: Publish Release Binaries
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags: [ 'v*.*.*' ]
|
||||
|
||||
@@ -8,6 +9,10 @@ jobs:
|
||||
goreleaser:
|
||||
name: goreleaser
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -37,6 +42,7 @@ jobs:
|
||||
go install github.com/sigstore/cosign/cmd/cosign@latest
|
||||
|
||||
- name: Run GoReleaser
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
workdir: "backend"
|
||||
@@ -45,3 +51,18 @@ jobs:
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COSIGN_PWD: ${{ secrets.COSIGN_PWD }}
|
||||
COSIGN_YES: "true"
|
||||
|
||||
- name: Run GoReleaser No Release
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
workdir: "backend"
|
||||
distribution: goreleaser
|
||||
version: "~> v2"
|
||||
args: release --clean --snapshot --skip=publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COSIGN_PWD: ${{ secrets.COSIGN_PWD }}
|
||||
COSIGN_YES: "true"
|
||||
52
.github/workflows/copilot-setup-steps.yml
vendored
Normal file
52
.github/workflows/copilot-setup-steps.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: "Copilot Setup Steps"
|
||||
|
||||
# Automatically run the setup steps when they are changed to allow for easy validation, and
|
||||
# allow manual testing through the repository's "Actions" tab
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/copilot-setup-steps.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/copilot-setup-steps.yml
|
||||
|
||||
jobs:
|
||||
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
|
||||
copilot-setup-steps:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Set the permissions to the lowest permissions possible needed for your steps.
|
||||
# Copilot will be given its own token for its operations.
|
||||
permissions:
|
||||
# If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete.
|
||||
contents: read
|
||||
|
||||
# You can define any steps you want, and they will run before the agent starts.
|
||||
# If you do not check out your code, Copilot will do this for you.
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- uses: pnpm/action-setup@v3.0.0
|
||||
with:
|
||||
version: 9.12.2
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
cache-dependency-path: backend/go.mod
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Perform setup
|
||||
run: task setup
|
||||
208
.github/workflows/docker-publish-hardened.yaml
vendored
Normal file
208
.github/workflows/docker-publish-hardened.yaml
vendored
Normal file
@@ -0,0 +1,208 @@
|
||||
name: Docker publish hardened
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '00 0 * * *'
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'frontend/**'
|
||||
- 'Dockerfile.hardened'
|
||||
- '.dockerignore'
|
||||
- '.github/workflows/docker-publish-hardened.yaml'
|
||||
tags: [ 'v*.*.*' ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'frontend/**'
|
||||
- 'Dockerfile.hardened'
|
||||
- '.dockerignore'
|
||||
- '.github/workflows/docker-publish-hardened.yaml'
|
||||
|
||||
permissions:
|
||||
contents: read # Access to repository contents
|
||||
packages: write # Write access for pushing to GHCR
|
||||
id-token: write # Required for OIDC authentication (if used)
|
||||
attestations: write # Required for signing and attestation (if needed)
|
||||
|
||||
env:
|
||||
DOCKERHUB_REPO: sysadminsmedia/homebox
|
||||
GHCR_REPO: ghcr.io/${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
|
||||
steps:
|
||||
- name: Enable Debug Logs
|
||||
run: echo "##[debug]Enabling debug logging"
|
||||
env:
|
||||
ACTIONS_RUNNER_DEBUG: true
|
||||
ACTIONS_STEP_DEBUG: true
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
echo "BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_ENV
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
branch=${{ github.event.pull_request.number || github.ref_name }}
|
||||
echo "BRANCH=${branch//\//-}" >> $GITHUB_ENV
|
||||
echo "DOCKERNAMES=${{ env.DOCKERHUB_REPO }},${{ env.GHCR_REPO }}" >> $GITHUB_ENV
|
||||
if [[ "${{ github.event_name }}" != "schedule" ]] || [[ "${{ github.ref }}" != refs/tags/* ]]; then
|
||||
echo "DOCKERNAMES=${{ env.GHCR_REPO }}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
||||
with:
|
||||
images: |
|
||||
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
|
||||
name=${{ env.GHCR_REPO }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392
|
||||
with:
|
||||
image: ghcr.io/amitie10g/binfmt:latest
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
with:
|
||||
driver-opts: |
|
||||
image=ghcr.io/amitie10g/buildkit:master
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
|
||||
with:
|
||||
context: . # Explicitly specify the build context
|
||||
file: ./Dockerfile.hardened # Explicitly specify the Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,"name=${{ env.DOCKERNAMES }}",push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
||||
cache-from: type=registry,ref=ghcr.io/sysadminsmedia/devcache:${{ env.PLATFORM_PAIR }}-${{ env.BRANCH }}-hardened
|
||||
cache-to: type=registry,ref=ghcr.io/sysadminsmedia/devcache:${{ env.PLATFORM_PAIR }}-${{ env.BRANCH }}-hardened,mode=max,ignore-error=true
|
||||
build-args: |
|
||||
VERSION=${{ github.ref_name }}
|
||||
COMMIT=${{ github.sha }}
|
||||
BUILD_TIME=${{ env.BUILD_TIME }}
|
||||
provenance: true
|
||||
sbom: true
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
needs:
|
||||
- build
|
||||
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
with:
|
||||
driver-opts: |
|
||||
image=ghcr.io/amitie10g/buildkit:master
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
||||
with:
|
||||
images: |
|
||||
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
|
||||
name=${{ env.GHCR_REPO }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=schedule,pattern=nightly
|
||||
flavor: |
|
||||
suffix=-hardened,onlatest=true
|
||||
|
||||
- name: Create manifest list and push GHCR
|
||||
id: push-ghcr
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
|
||||
|
||||
- name: Create manifest list and push Dockerhub
|
||||
id: push-dockerhub
|
||||
working-directory: /tmp/digests
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *)
|
||||
13
.github/workflows/docker-publish-rootless.yaml
vendored
13
.github/workflows/docker-publish-rootless.yaml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'frontend/**'
|
||||
- 'Dockerfile'
|
||||
- 'Dockerfile.rootless'
|
||||
- '.dockerignore'
|
||||
- '.github/workflows/docker-publish-rootless.yaml'
|
||||
ignore:
|
||||
@@ -19,7 +19,7 @@ on:
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'frontend/**'
|
||||
- 'Dockerfile'
|
||||
- 'Dockerfile.rootless'
|
||||
- '.dockerignore'
|
||||
- '.github/workflows/docker-publish-rootless.yaml'
|
||||
ignore:
|
||||
@@ -33,7 +33,7 @@ permissions:
|
||||
|
||||
env:
|
||||
DOCKERHUB_REPO: sysadminsmedia/homebox
|
||||
GHCR_REPO: ghcr.io/sysadminsmedia/homebox
|
||||
GHCR_REPO: ghcr.io/${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -51,7 +51,6 @@ jobs:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
- linux/riscv64
|
||||
|
||||
steps:
|
||||
- name: Enable Debug Logs
|
||||
@@ -84,7 +83,7 @@ jobs:
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -160,7 +159,7 @@ jobs:
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -205,7 +204,7 @@ jobs:
|
||||
- name: Create manifest list and push Dockerhub
|
||||
id: push-dockerhub
|
||||
working-directory: /tmp/digests
|
||||
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *)
|
||||
|
||||
9
.github/workflows/docker-publish.yaml
vendored
9
.github/workflows/docker-publish.yaml
vendored
@@ -27,7 +27,7 @@ on:
|
||||
|
||||
env:
|
||||
DOCKERHUB_REPO: sysadminsmedia/homebox
|
||||
GHCR_REPO: ghcr.io/sysadminsmedia/homebox
|
||||
GHCR_REPO: ghcr.io/${{ github.repository }}
|
||||
|
||||
permissions:
|
||||
contents: read # Access to repository contents
|
||||
@@ -51,7 +51,6 @@ jobs:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
- linux/riscv64
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -79,7 +78,7 @@ jobs:
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -153,6 +152,7 @@ jobs:
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -195,8 +195,7 @@ jobs:
|
||||
- name: Create manifest list and push Dockerhub
|
||||
id: push-dockerhub
|
||||
working-directory: /tmp/digests
|
||||
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *)
|
||||
|
||||
|
||||
5
.github/workflows/pull-requests.yaml
vendored
5
.github/workflows/pull-requests.yaml
vendored
@@ -9,7 +9,10 @@ on:
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'frontend/**'
|
||||
- '.github/workflows/**'
|
||||
- '.github/workflows/partial-backend.yaml'
|
||||
- '.github/workflows/partial-frontend.yaml'
|
||||
- '.github/workflows/e2e-partial.yaml'
|
||||
- '.github/workflows/pull-requests.yaml'
|
||||
|
||||
jobs:
|
||||
backend-tests:
|
||||
|
||||
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@@ -16,14 +16,12 @@
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"program": "${workspaceRoot}/backend/app/api/",
|
||||
"program": "${workspaceFolder}/backend/app/api/",
|
||||
"args": [],
|
||||
"env": {
|
||||
"HBOX_DEMO": "true",
|
||||
"HBOX_LOG_LEVEL": "debug",
|
||||
"HBOX_DEBUG_ENABLED": "true",
|
||||
"HBOX_STORAGE_DATA": "${workspaceRoot}/backend/.data",
|
||||
"HBOX_STORAGE_SQLITE_URL": "${workspaceRoot}/backend/.data/homebox.db?_fk=1&_time_format=sqlite"
|
||||
"HBOX_DEBUG_ENABLED": "true"
|
||||
},
|
||||
"console": "integratedTerminal",
|
||||
},
|
||||
@@ -46,4 +44,4 @@
|
||||
"console": "integratedTerminal",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
19
Dockerfile
19
Dockerfile
@@ -1,5 +1,5 @@
|
||||
# Node dependencies stage
|
||||
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/node:lts-alpine AS frontend-dependencies
|
||||
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-dependencies
|
||||
WORKDIR /app
|
||||
|
||||
# Install pnpm globally (caching layer)
|
||||
@@ -10,7 +10,7 @@ COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Build Nuxt (frontend) stage
|
||||
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/node:lts-alpine AS frontend-builder
|
||||
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-builder
|
||||
WORKDIR /app
|
||||
|
||||
# Install pnpm globally again (it can reuse the cache if not changed)
|
||||
@@ -22,7 +22,7 @@ COPY --from=frontend-dependencies /app/node_modules ./node_modules
|
||||
RUN pnpm build
|
||||
|
||||
# Go dependencies stage
|
||||
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
|
||||
FROM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
|
||||
WORKDIR /go/src/app
|
||||
|
||||
# Copy go.mod and go.sum for better caching
|
||||
@@ -30,7 +30,7 @@ COPY ./backend/go.mod ./backend/go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Build API stage
|
||||
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/golang:alpine AS builder
|
||||
FROM public.ecr.aws/docker/library/golang:alpine AS builder
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG BUILD_TIME
|
||||
@@ -40,7 +40,8 @@ ARG VERSION
|
||||
# Install necessary build tools
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
apk add --no-cache git build-base gcc g++
|
||||
apk add --no-cache git build-base gcc g++ && \
|
||||
if [ "$TARGETARCH" != "arm" ] || [ "$TARGETARCH" != "riscv64" ]; then apk --no-cache add libwebp libavif libheif libjxl; fi
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
@@ -55,17 +56,17 @@ COPY --from=frontend-builder /app/.output/public ./app/api/static/public
|
||||
# Use cache for Go build artifacts
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
if [ "$TARGETARCH" = "arm" ] || [ "$TARGETARCH" = "riscv64" ]; \
|
||||
then CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
|
||||
then echo "nodynamic" $TARGETOS $TARGETARCH; CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
|
||||
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
|
||||
-tags nodynamic -o /go/bin/api -v ./app/api/*.go; \
|
||||
else \
|
||||
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
|
||||
echo $TARGETOS $TARGETARCH; CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
|
||||
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
|
||||
-o /go/bin/api -v ./app/api/*.go; \
|
||||
fi
|
||||
|
||||
# Production stage
|
||||
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/alpine:latest
|
||||
FROM public.ecr.aws/docker/library/alpine:latest
|
||||
ENV HBOX_MODE=production
|
||||
ENV HBOX_STORAGE_CONN_STRING=file:///?no_tmp_dir=true
|
||||
ENV HBOX_STORAGE_PREFIX_PATH=data
|
||||
@@ -73,7 +74,7 @@ ENV HBOX_DATABASE_SQLITE_PATH=/data/homebox.db?_pragma=busy_timeout=2000&_pragma
|
||||
|
||||
# Install necessary runtime dependencies
|
||||
RUN apk --no-cache add ca-certificates wget && \
|
||||
if [ "$TARGETARCH" != "arm" ] || [ "$TARGETARCH" != "riscv64" ]; then apk --no-cache add libwebp libavif; fi
|
||||
if [ "$TARGETARCH" != "arm" ] || [ "$TARGETARCH" != "riscv64" ]; then apk --no-cache add libwebp libavif libheif libjxl; fi
|
||||
|
||||
# Create application directory and copy over built Go binary
|
||||
RUN mkdir /app
|
||||
|
||||
136
Dockerfile.hardened
Normal file
136
Dockerfile.hardened
Normal file
@@ -0,0 +1,136 @@
|
||||
# ---------------------------------------
|
||||
# Node dependencies stage
|
||||
# ---------------------------------------
|
||||
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-dependencies
|
||||
WORKDIR /app
|
||||
|
||||
# Install pnpm globally (caching layer)
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Copy package.json and lockfile to leverage caching
|
||||
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# ---------------------------------------
|
||||
# Build Nuxt (frontend) stage
|
||||
# ---------------------------------------
|
||||
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-builder
|
||||
WORKDIR /app
|
||||
|
||||
# Install pnpm globally again (it can reuse the cache if not changed)
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Copy over source files and node_modules from dependencies stage
|
||||
COPY frontend .
|
||||
COPY --from=frontend-dependencies /app/node_modules ./node_modules
|
||||
RUN pnpm build
|
||||
|
||||
# ---------------------------------------
|
||||
# Go dependencies stage
|
||||
# ---------------------------------------
|
||||
FROM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
|
||||
WORKDIR /go/src/app
|
||||
|
||||
# Copy go.mod and go.sum for better caching
|
||||
COPY ./backend/go.mod ./backend/go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# ---------------------------------------
|
||||
# Build API + healthcheck stage
|
||||
# ---------------------------------------
|
||||
FROM public.ecr.aws/docker/library/golang:alpine AS builder
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG BUILD_TIME
|
||||
ARG COMMIT
|
||||
ARG VERSION
|
||||
|
||||
# Install necessary build tools
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
apk add --no-cache git build-base gcc g++
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
# Copy Go modules (from dependencies stage) and source code
|
||||
COPY --from=builder-dependencies /go/pkg/mod /go/pkg/mod
|
||||
COPY ./backend .
|
||||
|
||||
# Clear old public files and copy new ones from frontend build
|
||||
RUN rm -rf ./app/api/public
|
||||
COPY --from=frontend-builder /app/.output/public ./app/api/static/public
|
||||
|
||||
# Use cache for Go build artifacts to build Homebox API
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
|
||||
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
|
||||
-tags nodynamic -o /go/bin/api -v ./app/api/*.go
|
||||
|
||||
RUN chmod +x /go/bin/api
|
||||
RUN mkdir /app
|
||||
RUN mkdir /data
|
||||
|
||||
# ---------- Build static healthcheck helper ----------
|
||||
# A small Go program that GETs the status URL and exits 0 on 2xx.
|
||||
RUN cat > /tmp/healthcheck.go <<'EOF'
|
||||
package main
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
func main() {
|
||||
url := "http://127.0.0.1:7745/api/v1/status"
|
||||
if len(os.Args) > 1 { url = os.Args[1] }
|
||||
c := &http.Client{ Timeout: 3 * time.Second }
|
||||
resp, err := c.Get(url)
|
||||
if err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode/100 != 2 {
|
||||
fmt.Fprintln(os.Stderr, "unexpected status:", resp.StatusCode)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
|
||||
go build -ldflags "-s -w" -o /go/bin/hc /tmp/healthcheck.go
|
||||
|
||||
# ---------------------------------------
|
||||
# Production stage
|
||||
# ---------------------------------------
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
ENV HBOX_MODE=production
|
||||
ENV HBOX_STORAGE_CONN_STRING=file:///?no_tmp_dir=true
|
||||
ENV HBOX_STORAGE_PREFIX_PATH=data
|
||||
ENV HBOX_DATABASE_SQLITE_PATH=/data/homebox.db?_pragma=busy_timeout=2000&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite
|
||||
|
||||
# Create application directory and copy over built Go binary and assets
|
||||
COPY --from=builder --chown=65532:65532 /app /app
|
||||
COPY --from=builder --chown=65532:65532 --chmod=755 /go/bin/api /app
|
||||
COPY --from=builder --chown=65532:65532 /data /data
|
||||
|
||||
# Copy the healthcheck helper
|
||||
COPY --from=builder --chown=65532:65532 --chmod=755 /go/bin/hc /app/healthcheck
|
||||
|
||||
# Labels and configuration for the final image
|
||||
LABEL Name=homebox Version=0.0.1
|
||||
LABEL org.opencontainers.image.source="https://github.com/sysadminsmedia/homebox"
|
||||
|
||||
# Expose necessary ports for Homebox
|
||||
EXPOSE 7745
|
||||
WORKDIR /app
|
||||
|
||||
# Persist volume for data
|
||||
VOLUME [ "/data" ]
|
||||
|
||||
# Entrypoint and CMD
|
||||
USER 65532
|
||||
ENTRYPOINT [ "/app/api" ]
|
||||
CMD [ "/data/config.yml" ]
|
||||
|
||||
# JSON exec-form healthcheck (no shell, no wget)
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
||||
CMD ["/app/healthcheck", "http://127.0.0.1:7745/api/v1/status"]
|
||||
@@ -1,5 +1,5 @@
|
||||
# Node dependencies stage
|
||||
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/node:lts-alpine AS frontend-dependencies
|
||||
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-dependencies
|
||||
WORKDIR /app
|
||||
|
||||
# Install pnpm globally (caching layer)
|
||||
@@ -10,7 +10,7 @@ COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Build Nuxt (frontend) stage
|
||||
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/node:lts-alpine AS frontend-builder
|
||||
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-builder
|
||||
WORKDIR /app
|
||||
|
||||
# Install pnpm globally again (it can reuse the cache if not changed)
|
||||
@@ -22,7 +22,7 @@ COPY --from=frontend-dependencies /app/node_modules ./node_modules
|
||||
RUN pnpm build
|
||||
|
||||
# Go dependencies stage
|
||||
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
|
||||
FROM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
|
||||
WORKDIR /go/src/app
|
||||
|
||||
# Copy go.mod and go.sum for better caching
|
||||
@@ -30,7 +30,9 @@ COPY ./backend/go.mod ./backend/go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Build API stage
|
||||
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/golang:alpine AS builder
|
||||
FROM public.ecr.aws/docker/library/golang:alpine AS builder
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG BUILD_TIME
|
||||
ARG COMMIT
|
||||
ARG VERSION
|
||||
@@ -38,7 +40,8 @@ ARG VERSION
|
||||
# Install necessary build tools
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
apk add --no-cache git build-base gcc g++
|
||||
apk add --no-cache git build-base gcc g++ && \
|
||||
if [ "$TARGETARCH" != "arm" ] || [ "$TARGETARCH" != "riscv64" ]; then apk --no-cache add libwebp libavif libheif libjxl; fi
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
@@ -53,11 +56,11 @@ COPY --from=frontend-builder /app/.output/public ./app/api/static/public
|
||||
# Use cache for Go build artifacts
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
if [ "$TARGETARCH" = "arm" ] || [ "$TARGETARCH" = "riscv64" ]; \
|
||||
then CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
|
||||
then echo "nodynamic" $TARGETOS $TARGETARCH; CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
|
||||
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
|
||||
-tags nodynamic -o /go/bin/api -v ./app/api/*.go; \
|
||||
else \
|
||||
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
|
||||
echo $TARGETOS $TARGETARCH; CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
|
||||
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
|
||||
-o /go/bin/api -v ./app/api/*.go; \
|
||||
fi
|
||||
@@ -65,7 +68,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
RUN mkdir /data
|
||||
|
||||
# Production stage
|
||||
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/alpine:latest
|
||||
FROM public.ecr.aws/docker/library/alpine:latest
|
||||
ENV HBOX_MODE=production
|
||||
ENV HBOX_STORAGE_CONN_STRING=file:///?no_tmp_dir=true
|
||||
ENV HBOX_STORAGE_PREFIX_PATH=data
|
||||
@@ -73,7 +76,7 @@ ENV HBOX_DATABASE_SQLITE_PATH=/data/homebox.db?_pragma=busy_timeout=2000&_pragma
|
||||
|
||||
# Install necessary runtime dependencies
|
||||
RUN apk --no-cache add ca-certificates wget && \
|
||||
if [ "$TARGETARCH" != "arm" ] || [ "$TARGETARCH" != "riscv64" ]; then apk --no-cache add libwebp libavif; fi
|
||||
if [ "$TARGETARCH" != "arm" ] || [ "$TARGETARCH" != "riscv64" ]; then apk --no-cache add libwebp libavif libheif libjxl; fi
|
||||
|
||||
# Create a nonroot user with UID/GID 65532
|
||||
RUN addgroup -g 65532 nonroot && adduser -u 65532 -G nonroot -S nonroot
|
||||
|
||||
@@ -14,6 +14,7 @@ builds:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
- freebsd
|
||||
goarch:
|
||||
- amd64
|
||||
- "386"
|
||||
@@ -25,22 +26,24 @@ builds:
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: "386"
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
- goos: freebsd
|
||||
goarch: "386"
|
||||
tags:
|
||||
- >-
|
||||
{{- if eq .Arch "riscv64" }}nodynamic
|
||||
{{- else if eq .Arch "arm" }}nodynamic
|
||||
{{- else if eq .Arch "386" }}nodynamic
|
||||
{{- else if eq .Os "freebsd" }}nodynamic
|
||||
{{ end }}
|
||||
sboms:
|
||||
- disable: false
|
||||
artifacts: any
|
||||
|
||||
signs:
|
||||
- cmd: cosign
|
||||
stdin: "{{ .Env.COSIGN_PWD }}"
|
||||
args:
|
||||
- "sign-blob"
|
||||
- "--key=cosign.key"
|
||||
- "--output-certificate=${certificate}"
|
||||
- "--output-signature=${signature}"
|
||||
- "${artifact}"
|
||||
- "--yes" # needed on cosign 2.0.0+
|
||||
|
||||
@@ -254,6 +254,25 @@ func (ctrl *V1Controller) HandleItemPatch() errchain.HandlerFunc {
|
||||
return adapters.ActionID("id", fn, http.StatusOK)
|
||||
}
|
||||
|
||||
// HandleItemDuplicate godocs
|
||||
//
|
||||
// @Summary Duplicate Item
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param payload body repo.DuplicateOptions true "Duplicate Options"
|
||||
// @Success 201 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id}/duplicate [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemDuplicate() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request, ID uuid.UUID, options repo.DuplicateOptions) (repo.ItemOut, error) {
|
||||
ctx := services.NewContext(r.Context())
|
||||
return ctrl.svc.Items.Duplicate(ctx, ctx.GID, ID, options)
|
||||
}
|
||||
|
||||
return adapters.ActionID("id", fn, http.StatusCreated)
|
||||
}
|
||||
|
||||
// HandleGetAllCustomFieldNames godocs
|
||||
//
|
||||
// @Summary Get All Custom Field Names
|
||||
|
||||
@@ -3,6 +3,7 @@ package v1
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -174,7 +175,7 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
|
||||
ctx := services.NewContext(r.Context())
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
doc, err := ctrl.svc.Items.AttachmentPath(r.Context(), attachmentID)
|
||||
doc, err := ctrl.svc.Items.AttachmentPath(r.Context(), ctx.GID, attachmentID)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to get attachment path")
|
||||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||
@@ -203,7 +204,9 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
|
||||
}
|
||||
}(bucket)
|
||||
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+doc.Title)
|
||||
// Set the Content-Disposition header for RFC6266 compliance
|
||||
disposition := "inline; filename*=UTF-8''" + url.QueryEscape(doc.Title)
|
||||
w.Header().Set("Content-Disposition", disposition)
|
||||
http.ServeContent(w, r, doc.Title, doc.CreatedAt, file)
|
||||
return nil
|
||||
|
||||
@@ -227,9 +230,9 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
attachment.ID = attachmentID
|
||||
val, err := ctrl.svc.Items.AttachmentUpdate(ctx, ID, &attachment)
|
||||
val, err := ctrl.svc.Items.AttachmentUpdate(ctx, ctx.GID, ID, &attachment)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to delete attachment")
|
||||
log.Err(err).Msg("failed to update attachment")
|
||||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ func generateOrPrint(ctrl *V1Controller, w http.ResponseWriter, r *http.Request,
|
||||
_, err = w.Write([]byte("Printed!"))
|
||||
return err
|
||||
} else {
|
||||
return labelmaker.GenerateLabel(w, ¶ms)
|
||||
return labelmaker.GenerateLabel(w, ¶ms, ctrl.config)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
332
backend/app/api/handlers/v1/v1_ctrl_product_search.go
Normal file
332
backend/app/api/handlers/v1/v1_ctrl_product_search.go
Normal file
@@ -0,0 +1,332 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hay-kot/httpkit/errchain"
|
||||
"github.com/hay-kot/httpkit/server"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/web/adapters"
|
||||
)
|
||||
|
||||
type UPCITEMDBResponse struct {
|
||||
Code string `json:"code"`
|
||||
Total int `json:"total"`
|
||||
Offset int `json:"offset"`
|
||||
Items []struct {
|
||||
Ean string `json:"ean"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Upc string `json:"upc"`
|
||||
Brand string `json:"brand"`
|
||||
Model string `json:"model"`
|
||||
Color string `json:"color"`
|
||||
Size string `json:"size"`
|
||||
Dimension string `json:"dimension"`
|
||||
Weight string `json:"weight"`
|
||||
Category string `json:"category"`
|
||||
LowestRecordedPrice float64 `json:"lowest_recorded_price"`
|
||||
HighestRecordedPrice float64 `json:"highest_recorded_price"`
|
||||
Images []string `json:"images"`
|
||||
Offers []struct {
|
||||
Merchant string `json:"merchant"`
|
||||
Domain string `json:"domain"`
|
||||
Title string `json:"title"`
|
||||
Currency string `json:"currency"`
|
||||
ListPrice string `json:"list_price"`
|
||||
Price float64 `json:"price"`
|
||||
Shipping string `json:"shipping"`
|
||||
Condition string `json:"condition"`
|
||||
Availability string `json:"availability"`
|
||||
Link string `json:"link"`
|
||||
UpdatedT int `json:"updated_t"`
|
||||
} `json:"offers"`
|
||||
Asin string `json:"asin"`
|
||||
Elid string `json:"elid"`
|
||||
} `json:"items"`
|
||||
}
|
||||
|
||||
type BARCODESPIDER_COMResponse struct {
|
||||
ItemResponse struct {
|
||||
Code int `json:"code"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
} `json:"item_response"`
|
||||
ItemAttributes struct {
|
||||
Title string `json:"title"`
|
||||
Upc string `json:"upc"`
|
||||
Ean string `json:"ean"`
|
||||
ParentCategory string `json:"parent_category"`
|
||||
Category string `json:"category"`
|
||||
Brand string `json:"brand"`
|
||||
Model string `json:"model"`
|
||||
Mpn string `json:"mpn"`
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
Publisher string `json:"publisher"`
|
||||
Asin string `json:"asin"`
|
||||
Color string `json:"color"`
|
||||
Size string `json:"size"`
|
||||
Weight string `json:"weight"`
|
||||
Image string `json:"image"`
|
||||
IsAdult string `json:"is_adult"`
|
||||
Description string `json:"description"`
|
||||
} `json:"item_attributes"`
|
||||
Stores []struct {
|
||||
StoreName string `json:"store_name"`
|
||||
Title string `json:"title"`
|
||||
Image string `json:"image"`
|
||||
Price string `json:"price"`
|
||||
Currency string `json:"currency"`
|
||||
Link string `json:"link"`
|
||||
Updated string `json:"updated"`
|
||||
} `json:"Stores"`
|
||||
}
|
||||
|
||||
// HandleGenerateQRCode godoc
|
||||
//
|
||||
// @Summary Search EAN from Barcode
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param data query string false "barcode to be searched"
|
||||
// @Success 200 {object} []repo.BarcodeProduct
|
||||
// @Router /v1/products/search-from-barcode [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleProductSearchFromBarcode(conf config.BarcodeAPIConf) errchain.HandlerFunc {
|
||||
type query struct {
|
||||
// 80 characters is the longest non-2D barcode length (GS1-128)
|
||||
EAN string `schema:"productEAN" validate:"required,max=80"`
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
q, err := adapters.DecodeQuery[query](r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const TIMEOUT_SEC = 10
|
||||
|
||||
log.Info().Msg("Processing barcode lookup request on: " + q.EAN)
|
||||
|
||||
// Search on UPCITEMDB
|
||||
var products []repo.BarcodeProduct
|
||||
|
||||
// www.ean-search.org/: not free
|
||||
|
||||
// Example code: dewalt 5035048748428
|
||||
|
||||
upcitemdb := func(iEan string) ([]repo.BarcodeProduct, error) {
|
||||
client := &http.Client{Timeout: TIMEOUT_SEC * time.Second}
|
||||
resp, err := client.Get("https://api.upcitemdb.com/prod/trial/lookup?upc=" + iEan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = errors.Join(err, resp.Body.Close())
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API returned status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// We Read the response body on the line below.
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Uncomment the following string for debug
|
||||
// sb := string(body)
|
||||
// log.Debug().Msg("Response: " + sb)
|
||||
|
||||
var result UPCITEMDBResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil { // Parse []byte to go struct pointer
|
||||
log.Error().Msg("Can not unmarshal JSON")
|
||||
}
|
||||
|
||||
var res []repo.BarcodeProduct
|
||||
|
||||
for _, it := range result.Items {
|
||||
var p repo.BarcodeProduct
|
||||
p.SearchEngineName = "upcitemdb.com"
|
||||
p.Barcode = iEan
|
||||
|
||||
p.Item.Description = it.Description
|
||||
p.Item.Name = it.Title
|
||||
p.Manufacturer = it.Brand
|
||||
p.ModelNumber = it.Model
|
||||
if len(it.Images) != 0 {
|
||||
p.ImageURL = it.Images[0]
|
||||
}
|
||||
|
||||
res = append(res, p)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
ps, err := upcitemdb(q.EAN)
|
||||
if err != nil {
|
||||
log.Error().Msg("Can not retrieve product from upcitemdb.com" + err.Error())
|
||||
}
|
||||
|
||||
// Barcode spider implementation
|
||||
barcodespider := func(tokenAPI string, iEan string) ([]repo.BarcodeProduct, error) {
|
||||
if len(tokenAPI) == 0 {
|
||||
return nil, errors.New("no api token configured for barcodespider. " +
|
||||
"Please define the api token in environment variable HBOX_BARCODE_TOKEN_BARCODESPIDER")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(
|
||||
"GET", "https://api.barcodespider.com/v1/lookup?upc="+iEan, nil)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("token", tokenAPI)
|
||||
|
||||
client := &http.Client{Timeout: TIMEOUT_SEC * time.Second}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// defer the call to Body.Close(). We also check the error code, and merge
|
||||
// it with the other error in this code to avoid error overiding.
|
||||
defer func() {
|
||||
err = errors.Join(err, resp.Body.Close())
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("barcodespider API returned status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// We Read the response body on the line below.
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Uncomment the following string for debug
|
||||
// sb := string(body)
|
||||
// log.Debug().Msg("Response: " + sb)
|
||||
|
||||
var result BARCODESPIDER_COMResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil { // Parse []byte to go struct pointer
|
||||
log.Error().Msg("Can not unmarshal JSON")
|
||||
}
|
||||
|
||||
// TODO: check 200 code on HTTP response.
|
||||
var p repo.BarcodeProduct
|
||||
p.Barcode = iEan
|
||||
p.SearchEngineName = "barcodespider.com"
|
||||
p.Item.Name = result.ItemAttributes.Title
|
||||
p.Item.Description = result.ItemAttributes.Description
|
||||
p.Manufacturer = result.ItemAttributes.Brand
|
||||
p.ModelNumber = result.ItemAttributes.Model
|
||||
p.ImageURL = result.ItemAttributes.Image
|
||||
|
||||
var res []repo.BarcodeProduct
|
||||
res = append(res, p)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
ps2, err := barcodespider(conf.TokenBarcodespider, q.EAN)
|
||||
if err != nil {
|
||||
log.Error().Msg("Can not retrieve product from barcodespider.com: " + err.Error())
|
||||
}
|
||||
|
||||
// Merge everything.
|
||||
products = append(products, ps...)
|
||||
|
||||
products = append(products, ps2...)
|
||||
|
||||
// Retrieve images if possible
|
||||
for i := range products {
|
||||
p := &products[i]
|
||||
|
||||
if len(p.ImageURL) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate URL is HTTPS
|
||||
u, err := url.Parse(p.ImageURL)
|
||||
if err != nil || u.Scheme != "https" {
|
||||
log.Warn().Msg("Skipping non-HTTPS image URL: " + p.ImageURL)
|
||||
continue
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: TIMEOUT_SEC * time.Second}
|
||||
res, err := client.Get(p.ImageURL)
|
||||
if err != nil {
|
||||
log.Warn().Msg("Cannot fetch image for URL: " + p.ImageURL + ": " + err.Error())
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = errors.Join(err, res.Body.Close())
|
||||
}()
|
||||
|
||||
// Validate response
|
||||
if res.StatusCode != http.StatusOK {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check content type
|
||||
contentType := res.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(contentType, "image/") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Limit image size to 8MB
|
||||
limitedReader := io.LimitReader(res.Body, 8*1024*1024)
|
||||
|
||||
// Read data of image
|
||||
bytes, err := io.ReadAll(limitedReader)
|
||||
if err != nil {
|
||||
log.Warn().Msg(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert to Base64
|
||||
var base64Encoding string
|
||||
|
||||
// Determine the content type of the image file
|
||||
mimeType := http.DetectContentType(bytes)
|
||||
|
||||
// Prepend the appropriate URI scheme header depending
|
||||
// on the MIME type
|
||||
switch mimeType {
|
||||
case "image/jpeg":
|
||||
base64Encoding += "data:image/jpeg;base64,"
|
||||
case "image/png":
|
||||
base64Encoding += "data:image/png;base64,"
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
// Append the base64 encoded output
|
||||
base64Encoding += base64.StdEncoding.EncodeToString(bytes)
|
||||
|
||||
p.ImageBase64 = base64Encoding
|
||||
}
|
||||
|
||||
if len(products) != 0 {
|
||||
return server.JSON(w, http.StatusOK, products)
|
||||
}
|
||||
|
||||
return server.JSON(w, http.StatusNoContent, nil)
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,9 @@ func (a *app) setupLogger() {
|
||||
|
||||
level, err := zerolog.ParseLevel(a.conf.Log.Level)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("level", a.conf.Log.Level).Msg("invalid log level, falling back to info")
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
} else {
|
||||
zerolog.SetGlobalLevel(level)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sysadminsmedia/homebox/backend/pkgs/utils"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/pressly/goose/v3"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/analytics"
|
||||
|
||||
"github.com/hay-kot/httpkit/errchain"
|
||||
"github.com/hay-kot/httpkit/graceful"
|
||||
@@ -28,16 +24,15 @@ import (
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/migrations"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/analytics"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/web/mid"
|
||||
"go.balki.me/anyhttp"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/sysadminsmedia/homebox/backend/internal/data/migrations/postgres"
|
||||
_ "github.com/sysadminsmedia/homebox/backend/internal/data/migrations/sqlite3"
|
||||
_ "github.com/sysadminsmedia/homebox/backend/pkgs/cgofreesqlite"
|
||||
|
||||
"gocloud.dev/pubsub"
|
||||
_ "gocloud.dev/pubsub/awssnssqs"
|
||||
_ "gocloud.dev/pubsub/azuresb"
|
||||
_ "gocloud.dev/pubsub/gcppubsub"
|
||||
@@ -102,79 +97,56 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func run(cfg *config.Config) error {
|
||||
app := new(cfg)
|
||||
app.setupLogger()
|
||||
|
||||
if cfg.Options.AllowAnalytics {
|
||||
analytics.Send(version, build())
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Initialize Database & Repos
|
||||
|
||||
if strings.HasPrefix(cfg.Storage.ConnString, "file:///./") {
|
||||
raw := strings.TrimPrefix(cfg.Storage.ConnString, "file:///./")
|
||||
clean := filepath.Clean(raw)
|
||||
absBase, err := filepath.Abs(clean)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to get absolute path for storage connection string")
|
||||
}
|
||||
// Construct and validate the full storage path
|
||||
storageDir := filepath.Join(absBase, cfg.Storage.PrefixPath)
|
||||
if !strings.HasPrefix(storageDir, absBase+string(os.PathSeparator)) && storageDir != absBase {
|
||||
log.Fatal().
|
||||
Str("path", storageDir).
|
||||
Msg("invalid storage path: you tried to use a prefix that is not a subdirectory of the base path")
|
||||
}
|
||||
// Create with more restrictive permissions
|
||||
if err := os.MkdirAll(storageDir, 0o750); err != nil {
|
||||
log.Fatal().
|
||||
Err(err).
|
||||
Msg("failed to create data directory")
|
||||
}
|
||||
err := setupStorageDir(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.ToLower(cfg.Database.Driver) == "postgres" {
|
||||
if !validatePostgresSSLMode(cfg.Database.SslMode) {
|
||||
log.Fatal().Str("sslmode", cfg.Database.SslMode).Msg("invalid sslmode")
|
||||
log.Error().Str("sslmode", cfg.Database.SslMode).Msg("invalid sslmode")
|
||||
return fmt.Errorf("invalid sslmode: %s", cfg.Database.SslMode)
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the database URL based on the driver because for some reason a common URL format is not used
|
||||
databaseURL := ""
|
||||
switch strings.ToLower(cfg.Database.Driver) {
|
||||
case "sqlite3":
|
||||
databaseURL = cfg.Database.SqlitePath
|
||||
|
||||
// Create directory for SQLite database if it doesn't exist
|
||||
dbFilePath := strings.Split(cfg.Database.SqlitePath, "?")[0] // Remove query parameters
|
||||
dbDir := filepath.Dir(dbFilePath)
|
||||
if err := os.MkdirAll(dbDir, 0o755); err != nil {
|
||||
log.Fatal().Err(err).Str("path", dbDir).Msg("failed to create SQLite database directory")
|
||||
}
|
||||
case "postgres":
|
||||
databaseURL = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", cfg.Database.Host, cfg.Database.Port, cfg.Database.Username, cfg.Database.Password, cfg.Database.Database, cfg.Database.SslMode)
|
||||
default:
|
||||
log.Fatal().Str("driver", cfg.Database.Driver).Msg("unsupported database driver")
|
||||
databaseURL, err := setupDatabaseURL(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := ent.Open(strings.ToLower(cfg.Database.Driver), databaseURL)
|
||||
if err != nil {
|
||||
log.Fatal().
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("driver", strings.ToLower(cfg.Database.Driver)).
|
||||
Str("host", cfg.Database.Host).
|
||||
Str("port", cfg.Database.Port).
|
||||
Str("database", cfg.Database.Database).
|
||||
Msg("failed opening connection to {driver} database at {host}:{port}/{database}")
|
||||
return fmt.Errorf("failed opening connection to %s database at %s:%s/%s: %w",
|
||||
strings.ToLower(cfg.Database.Driver),
|
||||
cfg.Database.Host,
|
||||
cfg.Database.Port,
|
||||
cfg.Database.Database,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
goose.SetBaseFS(migrations.Migrations(strings.ToLower(cfg.Database.Driver)))
|
||||
migrationsFs, err := migrations.Migrations(strings.ToLower(cfg.Database.Driver))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get migrations for %s: %w", strings.ToLower(cfg.Database.Driver), err)
|
||||
}
|
||||
|
||||
goose.SetBaseFS(migrationsFs)
|
||||
err = goose.SetDialect(strings.ToLower(cfg.Database.Driver))
|
||||
if err != nil {
|
||||
log.Fatal().Str("driver", cfg.Database.Driver).Msg("unsupported database driver")
|
||||
log.Error().Str("driver", cfg.Database.Driver).Msg("unsupported database driver")
|
||||
return fmt.Errorf("unsupported database driver: %s", cfg.Database.Driver)
|
||||
}
|
||||
|
||||
@@ -184,25 +156,9 @@ func run(cfg *config.Config) error {
|
||||
return err
|
||||
}
|
||||
|
||||
collectFuncs := []currencies.CollectorFunc{
|
||||
currencies.CollectDefaults(),
|
||||
}
|
||||
|
||||
if cfg.Options.CurrencyConfig != "" {
|
||||
log.Info().
|
||||
Str("path", cfg.Options.CurrencyConfig).
|
||||
Msg("loading currency config file")
|
||||
|
||||
content, err := os.ReadFile(cfg.Options.CurrencyConfig)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("path", cfg.Options.CurrencyConfig).
|
||||
Msg("failed to read currency config file")
|
||||
return err
|
||||
}
|
||||
|
||||
collectFuncs = append(collectFuncs, currencies.CollectJSON(bytes.NewReader(content)))
|
||||
collectFuncs, err := loadCurrencies(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currencies, err := currencies.CollectionCurrencies(collectFuncs...)
|
||||
@@ -256,154 +212,52 @@ func run(cfg *config.Config) error {
|
||||
_ = httpserver.Shutdown(context.Background())
|
||||
}()
|
||||
|
||||
listener, addrType, addrCfg, err := anyhttp.GetListener(cfg.Web.Host)
|
||||
if err == nil {
|
||||
switch addrType {
|
||||
case anyhttp.SystemdFD:
|
||||
sysdCfg := addrCfg.(*anyhttp.SysdConfig)
|
||||
if sysdCfg.IdleTimeout != nil {
|
||||
log.Error().Msg("idle timeout not yet supported. Please remove and try again")
|
||||
return errors.New("idle timeout not yet supported. Please remove and try again")
|
||||
}
|
||||
fallthrough
|
||||
case anyhttp.UnixSocket:
|
||||
log.Info().Msgf("Server is running on %s", cfg.Web.Host)
|
||||
return httpserver.Serve(listener)
|
||||
}
|
||||
} else {
|
||||
log.Debug().Msgf("anyhttp error: %v", err)
|
||||
}
|
||||
log.Info().Msgf("Server is running on %s:%s", cfg.Web.Host, cfg.Web.Port)
|
||||
return httpserver.ListenAndServe()
|
||||
})
|
||||
|
||||
// =========================================================================
|
||||
// Start Reoccurring Tasks
|
||||
registerRecurringTasks(app, cfg, runner)
|
||||
|
||||
runner.AddFunc("eventbus", app.bus.Run)
|
||||
|
||||
runner.AddFunc("seed_database", func(ctx context.Context) error {
|
||||
// TODO: Remove through external API that does setup
|
||||
if cfg.Demo {
|
||||
log.Info().Msg("Running in demo mode, creating demo data")
|
||||
err := app.SetupDemo()
|
||||
if err != nil {
|
||||
log.Fatal().Msg(err.Error())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
runner.AddPlugin(NewTask("purge-tokens", time.Duration(24)*time.Hour, func(ctx context.Context) {
|
||||
_, err := app.repos.AuthTokens.PurgeExpiredTokens(ctx)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("failed to purge expired tokens")
|
||||
}
|
||||
}))
|
||||
|
||||
runner.AddPlugin(NewTask("purge-invitations", time.Duration(24)*time.Hour, func(ctx context.Context) {
|
||||
_, err := app.repos.Groups.InvitationPurge(ctx)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("failed to purge expired invitations")
|
||||
}
|
||||
}))
|
||||
|
||||
runner.AddPlugin(NewTask("send-notifications", time.Duration(1)*time.Hour, func(ctx context.Context) {
|
||||
now := time.Now()
|
||||
|
||||
if now.Hour() == 8 {
|
||||
fmt.Println("run notifiers")
|
||||
err := app.services.BackgroundService.SendNotifiersToday(context.Background())
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("failed to send notifiers")
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
go runner.AddFunc("create-thumbnails-subscription", func(ctx context.Context) error {
|
||||
pubsubString, err := utils.GenerateSubPubConn(cfg.Database.PubSubConnString, "thumbnails")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to generate pubsub connection string")
|
||||
return err
|
||||
}
|
||||
topic, err := pubsub.OpenTopic(ctx, pubsubString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(topic *pubsub.Topic, ctx context.Context) {
|
||||
err := topic.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("fail to shutdown pubsub topic")
|
||||
}
|
||||
}(topic, ctx)
|
||||
|
||||
subscription, err := pubsub.OpenSubscription(ctx, pubsubString)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to open pubsub topic")
|
||||
return err
|
||||
}
|
||||
defer func(topic *pubsub.Subscription, ctx context.Context) {
|
||||
err := topic.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("fail to shutdown pubsub topic")
|
||||
}
|
||||
}(subscription, ctx)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
msg, err := subscription.Receive(ctx)
|
||||
log.Debug().Msg("received thumbnail generation request from pubsub topic")
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to receive message from pubsub topic")
|
||||
// Send analytics if enabled at around midnight UTC
|
||||
if cfg.Options.AllowAnalytics {
|
||||
analyticsTime := time.Second
|
||||
runner.AddPlugin(NewTask("send-analytics", analyticsTime, func(ctx context.Context) {
|
||||
for {
|
||||
now := time.Now().UTC()
|
||||
nextMidnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC)
|
||||
dur := time.Until(nextMidnight)
|
||||
analyticsTime = dur
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(dur):
|
||||
log.Debug().Msg("running send analytics")
|
||||
err := analytics.Send(version, build())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to send analytics")
|
||||
}
|
||||
}
|
||||
groupId, err := uuid.Parse(msg.Metadata["group_id"])
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("group_id", msg.Metadata["group_id"]).
|
||||
Msg("failed to parse group ID from message metadata")
|
||||
}
|
||||
attachmentId, err := uuid.Parse(msg.Metadata["attachment_id"])
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("attachment_id", msg.Metadata["attachment_id"]).
|
||||
Msg("failed to parse attachment ID from message metadata")
|
||||
}
|
||||
err = app.repos.Attachments.CreateThumbnail(ctx, groupId, attachmentId, msg.Metadata["title"], msg.Metadata["path"])
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to create thumbnail")
|
||||
}
|
||||
msg.Ack()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if cfg.Options.GithubReleaseCheck {
|
||||
runner.AddPlugin(NewTask("get-latest-github-release", time.Hour, func(ctx context.Context) {
|
||||
log.Debug().Msg("running get latest github release")
|
||||
err := app.services.BackgroundService.GetLatestGithubRelease(context.Background())
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("failed to get latest github release")
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
if cfg.Debug.Enabled {
|
||||
runner.AddFunc("debug", func(ctx context.Context) error {
|
||||
debugserver := http.Server{
|
||||
Addr: fmt.Sprintf("%s:%s", cfg.Web.Host, cfg.Debug.Port),
|
||||
Handler: app.debugRouter(),
|
||||
ReadTimeout: cfg.Web.ReadTimeout,
|
||||
WriteTimeout: cfg.Web.WriteTimeout,
|
||||
IdleTimeout: cfg.Web.IdleTimeout,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = debugserver.Shutdown(context.Background())
|
||||
}()
|
||||
|
||||
log.Info().Msgf("Debug server is running on %s:%s", cfg.Web.Host, cfg.Debug.Port)
|
||||
return debugserver.ListenAndServe()
|
||||
})
|
||||
// Print the configuration to the console
|
||||
cfg.Print()
|
||||
}
|
||||
|
||||
return runner.Start(context.Background())
|
||||
}
|
||||
|
||||
151
backend/app/api/recurring.go
Normal file
151
backend/app/api/recurring.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/httpkit/graceful"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
||||
"github.com/sysadminsmedia/homebox/backend/pkgs/utils"
|
||||
"gocloud.dev/pubsub"
|
||||
)
|
||||
|
||||
func registerRecurringTasks(app *app, cfg *config.Config, runner *graceful.Runner) {
|
||||
runner.AddFunc("eventbus", app.bus.Run)
|
||||
|
||||
runner.AddFunc("seed_database", func(ctx context.Context) error {
|
||||
if cfg.Demo {
|
||||
log.Info().Msg("Running in demo mode, creating demo data")
|
||||
err := app.SetupDemo()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to setup demo data")
|
||||
return fmt.Errorf("failed to setup demo data: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
runner.AddPlugin(NewTask("purge-tokens", 24*time.Hour, func(ctx context.Context) {
|
||||
_, err := app.repos.AuthTokens.PurgeExpiredTokens(ctx)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to purge expired tokens")
|
||||
}
|
||||
}))
|
||||
|
||||
runner.AddPlugin(NewTask("purge-invitations", 24*time.Hour, func(ctx context.Context) {
|
||||
_, err := app.repos.Groups.InvitationPurge(ctx)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to purge expired invitations")
|
||||
}
|
||||
}))
|
||||
|
||||
runner.AddPlugin(NewTask("send-notifications", time.Hour, func(ctx context.Context) {
|
||||
now := time.Now()
|
||||
if now.Hour() == 8 {
|
||||
fmt.Println("run notifiers")
|
||||
err := app.services.BackgroundService.SendNotifiersToday(context.Background())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to send notifiers")
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
if cfg.Thumbnail.Enabled {
|
||||
runner.AddFunc("create-thumbnails-subscription", func(ctx context.Context) error {
|
||||
pubsubString, err := utils.GenerateSubPubConn(cfg.Database.PubSubConnString, "thumbnails")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to generate pubsub connection string")
|
||||
return err
|
||||
}
|
||||
topic, err := pubsub.OpenTopic(ctx, pubsubString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(topic *pubsub.Topic, ctx context.Context) {
|
||||
err := topic.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("fail to shutdown pubsub topic")
|
||||
}
|
||||
}(topic, ctx)
|
||||
|
||||
subscription, err := pubsub.OpenSubscription(ctx, pubsubString)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to open pubsub topic")
|
||||
return err
|
||||
}
|
||||
defer func(topic *pubsub.Subscription, ctx context.Context) {
|
||||
err := topic.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("fail to shutdown pubsub topic")
|
||||
}
|
||||
}(subscription, ctx)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
msg, err := subscription.Receive(ctx)
|
||||
log.Debug().Msg("received thumbnail generation request from pubsub topic")
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to receive message from pubsub topic")
|
||||
continue
|
||||
}
|
||||
if msg == nil {
|
||||
log.Warn().Msg("received nil message from pubsub topic")
|
||||
continue
|
||||
}
|
||||
groupId, err := uuid.Parse(msg.Metadata["group_id"])
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("group_id", msg.Metadata["group_id"]).Msg("failed to parse group ID from message metadata")
|
||||
}
|
||||
attachmentId, err := uuid.Parse(msg.Metadata["attachment_id"])
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("attachment_id", msg.Metadata["attachment_id"]).Msg("failed to parse attachment ID from message metadata")
|
||||
}
|
||||
err = app.repos.Attachments.CreateThumbnail(ctx, groupId, attachmentId, msg.Metadata["title"], msg.Metadata["path"])
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to create thumbnail")
|
||||
}
|
||||
msg.Ack()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if cfg.Options.GithubReleaseCheck {
|
||||
runner.AddPlugin(NewTask("get-latest-github-release", time.Hour, func(ctx context.Context) {
|
||||
log.Debug().Msg("running get latest github release")
|
||||
err := app.services.BackgroundService.GetLatestGithubRelease(context.Background())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get latest github release")
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
if cfg.Debug.Enabled {
|
||||
runner.AddFunc("debug", func(ctx context.Context) error {
|
||||
debugserver := http.Server{
|
||||
Addr: fmt.Sprintf("%s:%s", cfg.Web.Host, cfg.Debug.Port),
|
||||
Handler: app.debugRouter(),
|
||||
ReadTimeout: cfg.Web.ReadTimeout,
|
||||
WriteTimeout: cfg.Web.WriteTimeout,
|
||||
IdleTimeout: cfg.Web.IdleTimeout,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = debugserver.Shutdown(context.Background())
|
||||
}()
|
||||
|
||||
log.Info().Msgf("Debug server is running on %s:%s", cfg.Web.Host, cfg.Debug.Port)
|
||||
return debugserver.ListenAndServe()
|
||||
})
|
||||
// Print the configuration to the console
|
||||
cfg.Print()
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
|
||||
r.Put("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...))
|
||||
r.Patch("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemPatch(), userMW...))
|
||||
r.Delete("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...))
|
||||
r.Post("/items/{id}/duplicate", chain.ToHandlerFunc(v1Ctrl.HandleItemDuplicate(), userMW...))
|
||||
|
||||
r.Post("/items/{id}/attachments", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...))
|
||||
r.Put("/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...))
|
||||
@@ -157,6 +158,8 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
|
||||
a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
|
||||
}
|
||||
|
||||
r.Get("/products/search-from-barcode", chain.ToHandlerFunc(v1Ctrl.HandleProductSearchFromBarcode(a.conf.Barcode), userMW...))
|
||||
|
||||
r.Get("/qrcode", chain.ToHandlerFunc(v1Ctrl.HandleGenerateQRCode(), assetMW...))
|
||||
r.Get(
|
||||
"/items/{id}/attachments/{attachment_id}",
|
||||
|
||||
103
backend/app/api/setup.go
Normal file
103
backend/app/api/setup.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/core/currencies"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
||||
)
|
||||
|
||||
// setupStorageDir handles the creation and validation of the storage directory.
|
||||
func setupStorageDir(cfg *config.Config) error {
|
||||
if strings.HasPrefix(cfg.Storage.ConnString, "file:///./") {
|
||||
raw := strings.TrimPrefix(cfg.Storage.ConnString, "file:///./")
|
||||
clean := filepath.Clean(raw)
|
||||
absBase, err := filepath.Abs(clean)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get absolute path for storage connection string")
|
||||
return fmt.Errorf("failed to get absolute path for storage connection string: %w", err)
|
||||
}
|
||||
absBase = strings.ReplaceAll(absBase, "\\", "/")
|
||||
storageDir := filepath.Join(absBase, cfg.Storage.PrefixPath)
|
||||
storageDir = strings.ReplaceAll(storageDir, "\\", "/")
|
||||
if !strings.HasPrefix(storageDir, absBase+"/") && storageDir != absBase {
|
||||
log.Error().Str("path", storageDir).Msg("invalid storage path: you tried to use a prefix that is not a subdirectory of the base path")
|
||||
return fmt.Errorf("invalid storage path: you tried to use a prefix that is not a subdirectory of the base path")
|
||||
}
|
||||
if err := os.MkdirAll(storageDir, 0o750); err != nil {
|
||||
log.Error().Err(err).Msg("failed to create data directory")
|
||||
return fmt.Errorf("failed to create data directory: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupDatabaseURL returns the database URL and ensures any required directories exist.
|
||||
func setupDatabaseURL(cfg *config.Config) (string, error) {
|
||||
databaseURL := ""
|
||||
switch strings.ToLower(cfg.Database.Driver) {
|
||||
case "sqlite3":
|
||||
databaseURL = cfg.Database.SqlitePath
|
||||
dbFilePath := strings.Split(cfg.Database.SqlitePath, "?")[0]
|
||||
dbDir := filepath.Dir(dbFilePath)
|
||||
if err := os.MkdirAll(dbDir, 0o755); err != nil {
|
||||
log.Error().Err(err).Str("path", dbDir).Msg("failed to create SQLite database directory")
|
||||
return "", fmt.Errorf("failed to create SQLite database directory: %w", err)
|
||||
}
|
||||
case "postgres":
|
||||
databaseURL = fmt.Sprintf("host=%s port=%s dbname=%s sslmode=%s", cfg.Database.Host, cfg.Database.Port, cfg.Database.Database, cfg.Database.SslMode)
|
||||
if cfg.Database.Username != "" {
|
||||
databaseURL += fmt.Sprintf(" user=%s", cfg.Database.Username)
|
||||
}
|
||||
if cfg.Database.Password != "" {
|
||||
databaseURL += fmt.Sprintf(" password=%s", cfg.Database.Password)
|
||||
}
|
||||
if cfg.Database.SslRootCert != "" {
|
||||
if _, err := os.Stat(cfg.Database.SslRootCert); err != nil {
|
||||
log.Error().Err(err).Str("path", cfg.Database.SslRootCert).Msg("SSL root certificate file is not accessible")
|
||||
return "", fmt.Errorf("SSL root certificate file is not accessible: %w", err)
|
||||
}
|
||||
databaseURL += fmt.Sprintf(" sslrootcert=%s", cfg.Database.SslRootCert)
|
||||
}
|
||||
if cfg.Database.SslCert != "" {
|
||||
if _, err := os.Stat(cfg.Database.SslCert); err != nil {
|
||||
log.Error().Err(err).Str("path", cfg.Database.SslCert).Msg("SSL certificate file is not accessible")
|
||||
return "", fmt.Errorf("SSL certificate file is not accessible: %w", err)
|
||||
}
|
||||
databaseURL += fmt.Sprintf(" sslcert=%s", cfg.Database.SslCert)
|
||||
}
|
||||
if cfg.Database.SslKey != "" {
|
||||
if _, err := os.Stat(cfg.Database.SslKey); err != nil {
|
||||
log.Error().Err(err).Str("path", cfg.Database.SslKey).Msg("SSL key file is not accessible")
|
||||
return "", fmt.Errorf("SSL key file is not accessible: %w", err)
|
||||
}
|
||||
databaseURL += fmt.Sprintf(" sslkey=%s", cfg.Database.SslKey)
|
||||
}
|
||||
default:
|
||||
log.Error().Str("driver", cfg.Database.Driver).Msg("unsupported database driver")
|
||||
return "", fmt.Errorf("unsupported database driver: %s", cfg.Database.Driver)
|
||||
}
|
||||
return databaseURL, nil
|
||||
}
|
||||
|
||||
// loadCurrencies loads currency data from config if provided.
|
||||
func loadCurrencies(cfg *config.Config) ([]currencies.CollectorFunc, error) {
|
||||
collectFuncs := []currencies.CollectorFunc{
|
||||
currencies.CollectDefaults(),
|
||||
}
|
||||
if cfg.Options.CurrencyConfig != "" {
|
||||
log.Info().Str("path", cfg.Options.CurrencyConfig).Msg("loading currency config file")
|
||||
content, err := os.ReadFile(cfg.Options.CurrencyConfig)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", cfg.Options.CurrencyConfig).Msg("failed to read currency config file")
|
||||
return nil, err
|
||||
}
|
||||
collectFuncs = append(collectFuncs, currencies.CollectJSON(bytes.NewReader(content)))
|
||||
}
|
||||
return collectFuncs, nil
|
||||
}
|
||||
@@ -943,6 +943,48 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/items/{id}/duplicate": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
],
|
||||
"summary": "Duplicate Item",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Item ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Duplicate Options",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.DuplicateOptions"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.ItemOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/items/{id}/maintenance": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -1811,6 +1853,41 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/products/search-from-barcode": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
],
|
||||
"summary": "Search EAN from Barcode",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "barcode to be searched",
|
||||
"name": "data",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.BarcodeProduct"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/qrcode": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -3063,6 +3140,54 @@ const docTemplate = `{
|
||||
"TypeTime"
|
||||
]
|
||||
},
|
||||
"repo.BarcodeProduct": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"barcode": {
|
||||
"type": "string"
|
||||
},
|
||||
"imageBase64": {
|
||||
"type": "string"
|
||||
},
|
||||
"imageURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"item": {
|
||||
"$ref": "#/definitions/repo.ItemCreate"
|
||||
},
|
||||
"manufacturer": {
|
||||
"type": "string"
|
||||
},
|
||||
"modelNumber": {
|
||||
"description": "Identifications",
|
||||
"type": "string"
|
||||
},
|
||||
"notes": {
|
||||
"description": "Extras",
|
||||
"type": "string"
|
||||
},
|
||||
"search_engine_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.DuplicateOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"copyAttachments": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"copyCustomFields": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"copyMaintenance": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"copyPrefix": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.Group": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3573,7 +3698,7 @@ const docTemplate = `{
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
"maxLength": 1000
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
|
||||
@@ -941,6 +941,48 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/items/{id}/duplicate": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
],
|
||||
"summary": "Duplicate Item",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Item ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Duplicate Options",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.DuplicateOptions"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.ItemOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/items/{id}/maintenance": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -1809,6 +1851,41 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/products/search-from-barcode": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
],
|
||||
"summary": "Search EAN from Barcode",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "barcode to be searched",
|
||||
"name": "data",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.BarcodeProduct"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/qrcode": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -3061,6 +3138,54 @@
|
||||
"TypeTime"
|
||||
]
|
||||
},
|
||||
"repo.BarcodeProduct": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"barcode": {
|
||||
"type": "string"
|
||||
},
|
||||
"imageBase64": {
|
||||
"type": "string"
|
||||
},
|
||||
"imageURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"item": {
|
||||
"$ref": "#/definitions/repo.ItemCreate"
|
||||
},
|
||||
"manufacturer": {
|
||||
"type": "string"
|
||||
},
|
||||
"modelNumber": {
|
||||
"description": "Identifications",
|
||||
"type": "string"
|
||||
},
|
||||
"notes": {
|
||||
"description": "Extras",
|
||||
"type": "string"
|
||||
},
|
||||
"search_engine_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.DuplicateOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"copyAttachments": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"copyCustomFields": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"copyMaintenance": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"copyPrefix": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.Group": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3571,7 +3696,7 @@
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
"maxLength": 1000
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
|
||||
@@ -646,6 +646,38 @@ definitions:
|
||||
- TypeNumber
|
||||
- TypeBoolean
|
||||
- TypeTime
|
||||
repo.BarcodeProduct:
|
||||
properties:
|
||||
barcode:
|
||||
type: string
|
||||
imageBase64:
|
||||
type: string
|
||||
imageURL:
|
||||
type: string
|
||||
item:
|
||||
$ref: '#/definitions/repo.ItemCreate'
|
||||
manufacturer:
|
||||
type: string
|
||||
modelNumber:
|
||||
description: Identifications
|
||||
type: string
|
||||
notes:
|
||||
description: Extras
|
||||
type: string
|
||||
search_engine_name:
|
||||
type: string
|
||||
type: object
|
||||
repo.DuplicateOptions:
|
||||
properties:
|
||||
copyAttachments:
|
||||
type: boolean
|
||||
copyCustomFields:
|
||||
type: boolean
|
||||
copyMaintenance:
|
||||
type: boolean
|
||||
copyPrefix:
|
||||
type: string
|
||||
type: object
|
||||
repo.Group:
|
||||
properties:
|
||||
createdAt:
|
||||
@@ -991,7 +1023,7 @@ definitions:
|
||||
color:
|
||||
type: string
|
||||
description:
|
||||
maxLength: 255
|
||||
maxLength: 1000
|
||||
type: string
|
||||
name:
|
||||
maxLength: 255
|
||||
@@ -1947,6 +1979,32 @@ paths:
|
||||
summary: Update Item Attachment
|
||||
tags:
|
||||
- Items Attachments
|
||||
/v1/items/{id}/duplicate:
|
||||
post:
|
||||
parameters:
|
||||
- description: Item ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Duplicate Options
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/repo.DuplicateOptions'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
schema:
|
||||
$ref: '#/definitions/repo.ItemOut'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Duplicate Item
|
||||
tags:
|
||||
- Items
|
||||
/v1/items/{id}/maintenance:
|
||||
get:
|
||||
parameters:
|
||||
@@ -2543,6 +2601,27 @@ paths:
|
||||
summary: Test Notifier
|
||||
tags:
|
||||
- Notifiers
|
||||
/v1/products/search-from-barcode:
|
||||
get:
|
||||
parameters:
|
||||
- description: barcode to be searched
|
||||
in: query
|
||||
name: data
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/repo.BarcodeProduct'
|
||||
type: array
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Search EAN from Barcode
|
||||
tags:
|
||||
- Items
|
||||
/v1/qrcode:
|
||||
get:
|
||||
parameters:
|
||||
|
||||
215
backend/go.mod
215
backend/go.mod
@@ -1,102 +1,104 @@
|
||||
module github.com/sysadminsmedia/homebox/backend
|
||||
|
||||
go 1.24
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.3
|
||||
|
||||
require (
|
||||
entgo.io/ent v0.14.4
|
||||
entgo.io/ent v0.14.5
|
||||
github.com/ardanlabs/conf/v3 v3.8.0
|
||||
github.com/containrrr/shoutrrr v0.8.0
|
||||
github.com/evanoberholster/imagemeta v0.3.1
|
||||
github.com/gen2brain/avif v0.4.4
|
||||
github.com/gen2brain/heic v0.4.5
|
||||
github.com/gen2brain/jpegxl v0.4.5
|
||||
github.com/gen2brain/webp v0.5.5
|
||||
github.com/go-chi/chi/v5 v5.2.2
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/go-playground/validator/v10 v10.27.0
|
||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/schema v1.4.1
|
||||
github.com/hay-kot/httpkit v0.0.11
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/mattn/go-sqlite3 v1.14.28
|
||||
github.com/olahol/melody v1.2.1
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
github.com/olahol/melody v1.3.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pressly/goose/v3 v3.24.3
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.5
|
||||
github.com/shirou/gopsutil/v4 v4.25.7
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/swaggo/http-swagger/v2 v2.0.2
|
||||
github.com/swaggo/swag v1.16.4
|
||||
github.com/swaggo/swag v1.16.6
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0
|
||||
github.com/zeebo/blake3 v0.2.4
|
||||
gocloud.dev v0.41.0
|
||||
gocloud.dev/pubsub/kafkapubsub v0.41.0
|
||||
gocloud.dev/pubsub/natspubsub v0.41.0
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.41.0
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/image v0.28.0
|
||||
modernc.org/sqlite v1.37.1
|
||||
go.balki.me/anyhttp v0.5.2
|
||||
gocloud.dev v0.43.0
|
||||
gocloud.dev/pubsub/kafkapubsub v0.43.0
|
||||
gocloud.dev/pubsub/natspubsub v0.43.0
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.43.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/image v0.30.0
|
||||
golang.org/x/text v0.28.0
|
||||
modernc.org/sqlite v1.38.2
|
||||
)
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83 // indirect
|
||||
cel.dev/expr v0.22.1 // indirect
|
||||
cloud.google.com/go v0.120.0 // indirect
|
||||
cloud.google.com/go/auth v0.15.0 // indirect
|
||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 // indirect
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go v0.121.4 // indirect
|
||||
cloud.google.com/go/auth v0.16.4 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
cloud.google.com/go/iam v1.4.2 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.1 // indirect
|
||||
cloud.google.com/go/pubsub v1.48.0 // indirect
|
||||
cloud.google.com/go/storage v1.51.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.8.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.2 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||
cloud.google.com/go/pubsub v1.49.0 // indirect
|
||||
cloud.google.com/go/storage v1.55.0 // indirect
|
||||
github.com/Azure/azure-amqp-common-go/v3 v3.2.3 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.9.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 // indirect
|
||||
github.com/Azure/go-amqp v1.4.0 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
|
||||
github.com/IBM/sarama v1.45.1 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
|
||||
github.com/IBM/sarama v1.45.2 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/agext/levenshtein v1.2.1 // indirect
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.65 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect
|
||||
github.com/aws/smithy-go v1.22.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect
|
||||
github.com/aws/smithy-go v1.22.4 // indirect
|
||||
github.com/bmatcuk/doublestar v1.3.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/eapache/go-resiliency v1.7.0 // indirect
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect
|
||||
@@ -104,33 +106,33 @@ require (
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
github.com/fatih/color v1.15.0 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fogleman/gg v1.3.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||
github.com/go-openapi/spec v0.20.6 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.2 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/wire v0.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.13.0 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
|
||||
github.com/jcmturner/gofork v1.7.6 // indirect
|
||||
@@ -139,66 +141,67 @@ require (
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
|
||||
github.com/nats-io/nats.go v1.40.1 // indirect
|
||||
github.com/nats-io/nkeys v0.4.10 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/nats-io/nats.go v1.44.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
github.com/swaggo/files/v2 v2.0.0 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
||||
github.com/swaggo/files/v2 v2.0.2 // indirect
|
||||
github.com/tetratelabs/wazero v1.9.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/tinylib/msgp v1.3.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zclconf/go-cty v1.14.4 // indirect
|
||||
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
github.com/zeebo/errs v1.4.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/oauth2 v0.28.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/api v0.228.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/grpc v1.71.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
google.golang.org/api v0.247.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect
|
||||
google.golang.org/grpc v1.74.2 // indirect
|
||||
google.golang.org/protobuf v1.36.7 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.65.7 // indirect
|
||||
modernc.org/libc v1.66.7 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
551
backend/go.sum
551
backend/go.sum
@@ -1,48 +1,47 @@
|
||||
ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83 h1:nX4HXncwIdvQ8/8sIUIf1nyCkK8qdBaHQ7EtzPpuiGE=
|
||||
ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83/go.mod h1:Oe1xWPuu5q9LzyrWfbZmEZxFYeu4BHTyzfjeW2aZp/w=
|
||||
cel.dev/expr v0.22.1 h1:xoFEsNh972Yzey8N9TCPx2nDvMN7TMhQEzxLuj/iRrI=
|
||||
cel.dev/expr v0.22.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
|
||||
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
|
||||
cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps=
|
||||
cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
|
||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 h1:E0wvcUXTkgyN4wy4LGtNzMNGMytJN8afmIWXJVMi4cc=
|
||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9/go.mod h1:Oe1xWPuu5q9LzyrWfbZmEZxFYeu4BHTyzfjeW2aZp/w=
|
||||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs=
|
||||
cloud.google.com/go v0.121.4/go.mod h1:XEBchUiHFJbz4lKBZwYBDHV/rSyfFktk737TLDU089s=
|
||||
cloud.google.com/go/auth v0.16.4 h1:fXOAIQmkApVvcIn7Pc2+5J8QTMVbUGLscnSVNl11su8=
|
||||
cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
cloud.google.com/go/iam v1.4.2 h1:4AckGYAYsowXeHzsn/LCKWIwSWLkdb0eGjH8wWkd27Q=
|
||||
cloud.google.com/go/iam v1.4.2/go.mod h1:REGlrt8vSlh4dfCJfSEcNjLGq75wW75c5aU3FLOYq34=
|
||||
cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
|
||||
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
|
||||
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
|
||||
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
|
||||
cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
|
||||
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
|
||||
cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw=
|
||||
cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw=
|
||||
cloud.google.com/go/monitoring v1.24.1 h1:vKiypZVFD/5a3BbQMvI4gZdl8445ITzXFh257XBgrS0=
|
||||
cloud.google.com/go/monitoring v1.24.1/go.mod h1:Z05d1/vn9NaujqY2voG6pVQXoJGbp+r3laV+LySt9K0=
|
||||
cloud.google.com/go/pubsub v1.48.0 h1:ntFpQVrr10Wj/GXSOpxGmexGynldv/bFp25H0jy8aOs=
|
||||
cloud.google.com/go/pubsub v1.48.0/go.mod h1:AAtyjyIT/+zaY1ERKFJbefOvkUxRDNp3nD6TdfdqUZk=
|
||||
cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDlCw=
|
||||
cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc=
|
||||
cloud.google.com/go/trace v1.11.5 h1:CALS1loyxJMnRiCwZSpdf8ac7iCsjreMxFD2WGxzzHU=
|
||||
cloud.google.com/go/trace v1.11.5/go.mod h1:TwblCcqNInriu5/qzaeYEIH7wzUcchSdeY2l5wL3Eec=
|
||||
entgo.io/ent v0.14.4 h1:/DhDraSLXIkBhyiVoJeSshr4ZYi7femzhj6/TckzZuI=
|
||||
entgo.io/ent v0.14.4/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
|
||||
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
|
||||
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
|
||||
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
|
||||
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
|
||||
cloud.google.com/go/pubsub v1.49.0 h1:5054IkbslnrMCgA2MAEPcsN3Ky+AyMpEZcii/DoySPo=
|
||||
cloud.google.com/go/pubsub v1.49.0/go.mod h1:K1FswTWP+C1tI/nfi3HQecoVeFvL4HUOB1tdaNXKhUY=
|
||||
cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0=
|
||||
cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY=
|
||||
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
|
||||
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
|
||||
entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4=
|
||||
entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U=
|
||||
github.com/Azure/azure-amqp-common-go/v3 v3.2.3 h1:uDF62mbd9bypXWi19V1bN5NZEO84JqgmI5G73ibAmrk=
|
||||
github.com/Azure/azure-amqp-common-go/v3 v3.2.3/go.mod h1:7rPmbSfszeovxGfc5fSAXE4ehlXQZHpMja2OtxC2Tas=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0 h1:JNgM3Tz592fUHU2vgwgvOgKxo5s9Ki0y2wicBeckn70=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0/go.mod h1:6vUKmzY17h6dpn9ZLAhM4R/rcrltBeq52qZIkUR7Oro=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.9.1 h1:CRZwf68N55u7ZZo3Xx2ynuqEA6k5GZfwsEUkU8qsAPk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.9.1/go.mod h1:NydgUaroiShkgOcb+X6OUdS3RalWBrvDNtOyFHJtsZY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0 h1:LR0kAX9ykz8G4YgLCaRDVJ3+n43R8MneB5dTy2konZo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0/go.mod h1:DWAciXemNf++PQJLeXUB4HHH5OpsAh12HZnu2wXE1jA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 h1:lhZdRq7TIx0GJQvSyX2Si406vrYsov2FXGp/RnSEtcs=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+effbARHMQjgOKA2AYvcohNm7KEt42mSV8=
|
||||
github.com/Azure/go-amqp v0.17.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg=
|
||||
github.com/Azure/go-amqp v1.4.0 h1:Xj3caqi4comOF/L1Uc5iuBxR/pB6KumejC01YQOqOR4=
|
||||
github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE=
|
||||
@@ -60,91 +59,85 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
|
||||
github.com/IBM/sarama v1.45.1 h1:nY30XqYpqyXOXSNoe2XCgjj9jklGM1Ye94ierUb1jQ0=
|
||||
github.com/IBM/sarama v1.45.1/go.mod h1:qifDhA3VWSrQ1TjSMyxDl3nYL3oX2C83u+G6L79sq4w=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
|
||||
github.com/IBM/sarama v1.45.2 h1:8m8LcMCu3REcwpa7fCP6v2fuPuzVwXDAM2DOv3CBrKw=
|
||||
github.com/IBM/sarama v1.45.2/go.mod h1:ppaoTcVdGv186/z6MEKsMm70A5fwJfRTpstI37kVn3Y=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
|
||||
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
|
||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||
github.com/ardanlabs/conf/v3 v3.8.0 h1:Mvv2wZJz8tIl705m5BU3ZRCP1V6TKY6qebA8i4sykrY=
|
||||
github.com/ardanlabs/conf/v3 v3.8.0/go.mod h1:XlL9P0quWP4m1weOVFmlezabinbZLI05niDof/+Ochk=
|
||||
github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
|
||||
github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.12 h1:Y/2a+jLPrPbHpFkpAAYkVEtJmxORlXoo5k2g1fa2sUo=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.12/go.mod h1:xse1YTjmORlb/6fhkWi8qJh3cvZi4JoVNhc+NbJt4kI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.65 h1:q+nV2yYegofO/SUXruT+pn4KxkxmaQ++1B/QedcKBFM=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.65/go.mod h1:4zyjAuGOdikpNYiSGpsGz8hLGmUzlY8pc8r9QQ/RXYQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69 h1:6VFPH/Zi9xYFMJKPQOX5URYkQoXRWeJ7V/7Y6ZDYoms=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69/go.mod h1:GJj8mmO6YT6EqgduWocwhMoxTLFitkhIrK+owzrYL2I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
|
||||
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
|
||||
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 h1:jIiopHEV22b4yQP2q36Y0OmwLbsxNWdWwfZRR5QRRO4=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.2 h1:PajtbJ/5bEo6iUAIGMYnK8ljqg2F1h4mMCGh1acjN30=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.2/go.mod h1:PJtxxMdj747j8DeZENRTTYAz/lx/pADn/U0k7YNNiUY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3 h1:j5BchjfDoS7K26vPdyJlyxBIIBGDflq3qjjJKBDlbcI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3/go.mod h1:Bar4MrRxeqdn6XIh8JGfiXuFRmyrrsZNTJotxEJmWW0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 h1:pdgODsAhGo4dvzC3JAG5Ce0PX8kWXrTZGx+jxADD+5E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.2/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 h1:90uX0veLKcdHVfvxhkWUQSCi5VabtwMLFutYiRke4oo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
|
||||
github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
|
||||
github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 h1:0reDqfEN+tB+sozj2r92Bep8MEwBZgtAXTND1Kk9OXg=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.7 h1:OBuZE9Wt8h2imuRktu+WfjiTGrnYdCIJg8IX92aalHE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.7/go.mod h1:4WYoZAhHt+dWYpoOQUgkUKfuQbE6Gg/hW4oXE0pKS9U=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 h1:80dpSqWMwx2dAm30Ib7J6ucz1ZHfiv5OCRwN/EnCOXQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8/go.mod h1:IzNt/udsXlETCdvBOL0nmyMe2t9cGmXmZgsdoZGYYhI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=
|
||||
github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
|
||||
github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
|
||||
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k=
|
||||
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
|
||||
github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
@@ -158,20 +151,18 @@ github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
|
||||
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
github.com/evanoberholster/imagemeta v0.3.1 h1:E4GUjXcvlVMjP9joN25+bBNf3Al3MTTfMqCrDOCW+LE=
|
||||
github.com/evanoberholster/imagemeta v0.3.1/go.mod h1:V0vtDJmjTqvwAYO8r+u33NRVIMXQb0qSqEfImoKEiXM=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
||||
@@ -179,8 +170,8 @@ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzP
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gen2brain/avif v0.4.4 h1:Ga/ss7qcWWQm2bxFpnjYjhJsNfZrWs5RsyklgFjKRSE=
|
||||
github.com/gen2brain/avif v0.4.4/go.mod h1:/XCaJcjZraQwKVhpu9aEd9aLOssYOawLvhMBtmHVGqk=
|
||||
github.com/gen2brain/heic v0.4.5 h1:Cq3hPu6wwlTJNv2t48ro3oWje54h82Q5pALeCBNgaSk=
|
||||
@@ -191,33 +182,33 @@ github.com/gen2brain/webp v0.5.5 h1:MvQR75yIPU/9nSqYT5h13k4URaJK3gf9tgz/ksRbyEg=
|
||||
github.com/gen2brain/webp v0.5.5/go.mod h1:xOSMzp4aROt2KFW++9qcK/RBTOVC2S9tJG66ip/9Oc0=
|
||||
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
|
||||
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI=
|
||||
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
|
||||
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
|
||||
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA=
|
||||
github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
@@ -225,37 +216,18 @@ github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3a
|
||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=
|
||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
@@ -270,21 +242,20 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
|
||||
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
|
||||
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -293,8 +264,8 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc=
|
||||
github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
|
||||
github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZYSo=
|
||||
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
|
||||
github.com/hay-kot/httpkit v0.0.11 h1:ZdB2uqsFBSDpfUoClGK5c5orjBjQkEVSXh7fZX5FKEk=
|
||||
github.com/hay-kot/httpkit v0.0.11/go.mod h1:0kZdk5/swzdfqfg2c6pBWimcgeJ9PTyO97EbHnYl2Sw=
|
||||
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
|
||||
@@ -319,17 +290,14 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
|
||||
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
|
||||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
|
||||
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
@@ -340,47 +308,44 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
|
||||
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/nats-io/jwt/v2 v2.5.0 h1:WQQ40AAlqqfx+f6ku+i0pOVm+ASirD4fUh+oQsiE9Ak=
|
||||
github.com/nats-io/jwt/v2 v2.5.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI=
|
||||
github.com/nats-io/nats-server/v2 v2.9.23 h1:6Wj6H6QpP9FMlpCyWUaNu2yeZ/qGj+mdRkZ1wbikExU=
|
||||
github.com/nats-io/nats-server/v2 v2.9.23/go.mod h1:wEjrEy9vnqIGE4Pqz4/c75v9Pmaq7My2IgFmnykc4C0=
|
||||
github.com/nats-io/nats.go v1.40.1 h1:MLjDkdsbGUeCMKFyCFoLnNn/HDTqcgVa3EQm+pMNDPk=
|
||||
github.com/nats-io/nats.go v1.40.1/go.mod h1:wV73x0FSI/orHPSYoyMeJB+KajMDoWyXmFaRrrYaaTo=
|
||||
github.com/nats-io/nkeys v0.4.10 h1:glmRrpCmYLHByYcePvnTBEAwawwapjCPMjy2huw20wc=
|
||||
github.com/nats-io/nkeys v0.4.10/go.mod h1:OjRrnIKnWBFl+s4YK5ChQfvHP2fxqZexrKJoVVyWB3U=
|
||||
github.com/nats-io/nats.go v1.44.0 h1:ECKVrDLdh/kDPV1g0gAQ+2+m2KprqZK5O/eJAyAnH2M=
|
||||
github.com/nats-io/nats.go v1.44.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/olahol/melody v1.2.1 h1:xdwRkzHxf+B0w4TKbGpUSSkV516ZucQZJIWLztOWICQ=
|
||||
github.com/olahol/melody v1.2.1/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/olahol/melody v1.3.0 h1:n7UlKiQnxVrgxKoM0d7usZiN+Z0y2lVENtYLgKtXS6s=
|
||||
github.com/olahol/melody v1.3.0/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
|
||||
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
|
||||
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
|
||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
@@ -389,19 +354,19 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/pressly/goose/v3 v3.24.3 h1:DSWWNwwggVUsYZ0X2VitiAa9sKuqtBfe+Jr9zFGwWlM=
|
||||
github.com/pressly/goose/v3 v3.24.3/go.mod h1:v9zYL4xdViLHCUUJh/mhjnm6JrK7Eul8AS93IxiZM4E=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
|
||||
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
@@ -409,18 +374,16 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
|
||||
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
|
||||
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -432,18 +395,20 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
||||
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
|
||||
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
|
||||
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
|
||||
github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg=
|
||||
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
|
||||
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
|
||||
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0 h1:chdyhEfRtUPgQtuPeaWVGQ/TQx4rE1PqeoW3U+53t34=
|
||||
@@ -461,73 +426,65 @@ github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
||||
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
|
||||
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.balki.me/anyhttp v0.5.2 h1:et4tCDXLeXpWfMNvRKG7ojfrnlr3du7cEaG966MLSpA=
|
||||
go.balki.me/anyhttp v0.5.2/go.mod h1:JhfekOIjgVODoVqUCficjpIgmB3wwlB7jhN0eN2EZ/s=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 h1:6VjV6Et+1Hd2iLZEPtdV7vie80Yyqf7oikJLjQ/myi0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0/go.mod h1:u8hcp8ji5gaM/RfcOo8z9NMnf1pVLfVY7lBY2VOGuUU=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
gocloud.dev v0.41.0 h1:qBKd9jZkBKEghYbP/uThpomhedK5s2Gy6Lz7h/zYYrM=
|
||||
gocloud.dev v0.41.0/go.mod h1:IetpBcWLUwroOOxKr90lhsZ8vWxeSkuszBnW62sbcf0=
|
||||
gocloud.dev/pubsub/kafkapubsub v0.41.0 h1:Ft6YB77ejqk++VjW51UP39RH/WDAMtv6ed3+PHMxBzg=
|
||||
gocloud.dev/pubsub/kafkapubsub v0.41.0/go.mod h1:kJf4c6b+4yJk6nXmv33yXKblbrgWmrYCzI5QEsr27G0=
|
||||
gocloud.dev/pubsub/natspubsub v0.41.0 h1:UxNb0DiAzdnyHut6jcCG7u6lsB/hzxTyZ/RHWeCUJ4Q=
|
||||
gocloud.dev/pubsub/natspubsub v0.41.0/go.mod h1:uCBKjwvIcuNuf3+ft4wUI9hPHHKQvroxq9ZPB/410ac=
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.41.0 h1:RutvHbacZxlFr0t3wlr+kz63j53UOfHY3PJR8NKN1EI=
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.41.0/go.mod h1:s7oQXOlQ2FOj8XmYMv5Ocgs1t+8hIXfsKaWGgECM9SQ=
|
||||
gocloud.dev v0.43.0 h1:aW3eq4RMyehbJ54PMsh4hsp7iX8cO/98ZRzJJOzN/5M=
|
||||
gocloud.dev v0.43.0/go.mod h1:eD8rkg7LhKUHrzkEdLTZ+Ty/vgPHPCd+yMQdfelQVu4=
|
||||
gocloud.dev/pubsub/kafkapubsub v0.43.0 h1:Kgwi0na69W3RgxEffEkdrMhox6A3Q0gajoJtjHGVr/s=
|
||||
gocloud.dev/pubsub/kafkapubsub v0.43.0/go.mod h1:uKI0CXuj7HJ/YnnOLQ3VkDnuUnkz+q/d+tRzmfhmOOU=
|
||||
gocloud.dev/pubsub/natspubsub v0.43.0 h1:k35tFoaorvD9Fa26zVEEzyXiMOEyXNHc0pBOmRYvQI0=
|
||||
gocloud.dev/pubsub/natspubsub v0.43.0/go.mod h1:xJn8TO8pGYieDn6AsRFsYfhQW8cnC+xGmG9APGNxkpQ=
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.43.0 h1:6nNZFSlJ1dk2GujL8PFltfLz3vC6IbrpjGS4FTduo1s=
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.43.0/go.mod h1:sEaueAGat+OASRoB3QDkghCtibKttgg7X6zsPTm1pl0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
|
||||
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
||||
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
@@ -535,25 +492,20 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -564,11 +516,10 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -582,60 +533,35 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/api v0.228.0 h1:X2DJ/uoWGnY5obVjewbp8icSL5U4FzuCfy9OjbLSnLs=
|
||||
google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 h1:qEFnJI6AnfZk0NNe8YTyXQh5i//Zxi4gBHwRgp76qpw=
|
||||
google.golang.org/genproto v0.0.0-20250324211829-b45e905df463/go.mod h1:SqIx1NV9hcvqdLHo7uNZDS5lrUJybQ3evo3+z/WBfA0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/api v0.247.0 h1:tSd/e0QrUlLsrwMKmkbQhYVa109qIintOls2Wh6bngc=
|
||||
google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM=
|
||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs=
|
||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79 h1:iOye66xuaAK0WnkPuhQPUFy8eJcmwUXqGGP3om6IxX8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79/go.mod h1:HKJDgKsFUnv5VAGeQjz8kxcgDP0HoE0iZNp0OdZNlhE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a h1:tPE/Kp+x9dMSwUm/uM0JKK0IfdiJkwAbSMSeZBXXJXc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
|
||||
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -643,21 +569,20 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
|
||||
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
|
||||
modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
|
||||
modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
|
||||
modernc.org/fileutil v1.3.15/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
|
||||
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
|
||||
modernc.org/libc v1.66.7/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -666,8 +591,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs=
|
||||
modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g=
|
||||
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -38,6 +38,10 @@ func (svc *ItemService) Create(ctx Context, item repo.ItemCreate) (repo.ItemOut,
|
||||
return svc.repo.Items.Create(ctx, ctx.GID, item)
|
||||
}
|
||||
|
||||
func (svc *ItemService) Duplicate(ctx Context, gid, id uuid.UUID, options repo.DuplicateOptions) (repo.ItemOut, error) {
|
||||
return svc.repo.Items.Duplicate(ctx, gid, id, options)
|
||||
}
|
||||
|
||||
func (svc *ItemService) EnsureAssetID(ctx context.Context, gid uuid.UUID) (int, error) {
|
||||
items, err := svc.repo.Items.GetAllZeroAssetID(ctx, gid)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
func (svc *ItemService) AttachmentPath(ctx context.Context, attachmentID uuid.UUID) (*ent.Attachment, error) {
|
||||
attachment, err := svc.repo.Attachments.Get(ctx, attachmentID)
|
||||
func (svc *ItemService) AttachmentPath(ctx context.Context, gid uuid.UUID, attachmentID uuid.UUID) (*ent.Attachment, error) {
|
||||
attachment, err := svc.repo.Attachments.Get(ctx, gid, attachmentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -19,16 +19,16 @@ func (svc *ItemService) AttachmentPath(ctx context.Context, attachmentID uuid.UU
|
||||
return attachment, nil
|
||||
}
|
||||
|
||||
func (svc *ItemService) AttachmentUpdate(ctx Context, itemID uuid.UUID, data *repo.ItemAttachmentUpdate) (repo.ItemOut, error) {
|
||||
func (svc *ItemService) AttachmentUpdate(ctx Context, gid uuid.UUID, itemID uuid.UUID, data *repo.ItemAttachmentUpdate) (repo.ItemOut, error) {
|
||||
// Update Attachment
|
||||
attachment, err := svc.repo.Attachments.Update(ctx, data.ID, data)
|
||||
attachment, err := svc.repo.Attachments.Update(ctx, gid, data.ID, data)
|
||||
if err != nil {
|
||||
return repo.ItemOut{}, err
|
||||
}
|
||||
|
||||
// Update Document
|
||||
attDoc := attachment
|
||||
_, err = svc.repo.Attachments.Rename(ctx, attDoc.ID, data.Title)
|
||||
_, err = svc.repo.Attachments.Rename(ctx, gid, attDoc.ID, data.Title)
|
||||
if err != nil {
|
||||
return repo.ItemOut{}, err
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func (svc *ItemService) AttachmentAdd(ctx Context, itemID uuid.UUID, filename st
|
||||
|
||||
func (svc *ItemService) AttachmentDelete(ctx context.Context, gid uuid.UUID, id uuid.UUID, attachmentID uuid.UUID) error {
|
||||
// Delete the attachment
|
||||
err := svc.repo.Attachments.Delete(ctx, attachmentID)
|
||||
err := svc.repo.Attachments.Delete(ctx, gid, id, attachmentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
120
backend/internal/data/ent/item_predicates.go
Normal file
120
backend/internal/data/ent/item_predicates.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package ent
|
||||
|
||||
import (
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/item"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/predicate"
|
||||
"github.com/sysadminsmedia/homebox/backend/pkgs/textutils"
|
||||
)
|
||||
|
||||
// AccentInsensitiveContains creates a predicate that performs accent-insensitive text search.
|
||||
// It normalizes both the database field value and the search value for comparison.
|
||||
func AccentInsensitiveContains(field string, searchValue string) predicate.Item {
|
||||
if searchValue == "" {
|
||||
return predicate.Item(func(s *sql.Selector) {
|
||||
// Return a predicate that never matches if search is empty
|
||||
s.Where(sql.False())
|
||||
})
|
||||
}
|
||||
|
||||
// Normalize the search value
|
||||
normalizedSearch := textutils.NormalizeSearchQuery(searchValue)
|
||||
|
||||
return predicate.Item(func(s *sql.Selector) {
|
||||
dialect := s.Dialect()
|
||||
|
||||
switch dialect {
|
||||
case "sqlite3":
|
||||
// For SQLite, we'll create a custom normalization function using REPLACE
|
||||
// to handle common accented characters
|
||||
normalizeFunc := buildSQLiteNormalizeExpression(s.C(field))
|
||||
s.Where(sql.ExprP(
|
||||
"LOWER("+normalizeFunc+") LIKE ?",
|
||||
"%"+normalizedSearch+"%",
|
||||
))
|
||||
case "postgres":
|
||||
// For PostgreSQL, use REPLACE-based normalization to avoid unaccent dependency
|
||||
normalizeFunc := buildGenericNormalizeExpression(s.C(field))
|
||||
// Use sql.P() for proper PostgreSQL parameter binding ($1, $2, etc.)
|
||||
s.Where(sql.P(func(b *sql.Builder) {
|
||||
b.WriteString("LOWER(")
|
||||
b.WriteString(normalizeFunc)
|
||||
b.WriteString(") LIKE ")
|
||||
b.Arg("%" + normalizedSearch + "%")
|
||||
}))
|
||||
default:
|
||||
// Default fallback using REPLACE for common accented characters
|
||||
normalizeFunc := buildGenericNormalizeExpression(s.C(field))
|
||||
s.Where(sql.ExprP(
|
||||
"LOWER("+normalizeFunc+") LIKE ?",
|
||||
"%"+normalizedSearch+"%",
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// buildSQLiteNormalizeExpression creates a SQLite expression to normalize accented characters
|
||||
func buildSQLiteNormalizeExpression(fieldExpr string) string {
|
||||
return buildGenericNormalizeExpression(fieldExpr)
|
||||
}
|
||||
|
||||
// buildGenericNormalizeExpression creates a database-agnostic expression to normalize common accented characters
|
||||
func buildGenericNormalizeExpression(fieldExpr string) string {
|
||||
// Chain REPLACE functions to handle the most common accented characters
|
||||
// Focused on the most frequently used accents in Spanish, French, and Portuguese
|
||||
// Ordered by frequency of use for better performance
|
||||
normalized := fieldExpr
|
||||
|
||||
// Most common accented characters ordered by frequency
|
||||
commonAccents := []struct {
|
||||
from, to string
|
||||
}{
|
||||
// Spanish - most common
|
||||
{"á", "a"}, {"é", "e"}, {"í", "i"}, {"ó", "o"}, {"ú", "u"}, {"ñ", "n"},
|
||||
{"Á", "A"}, {"É", "E"}, {"Í", "I"}, {"Ó", "O"}, {"Ú", "U"}, {"Ñ", "N"},
|
||||
|
||||
// French - most common
|
||||
{"è", "e"}, {"ê", "e"}, {"à", "a"}, {"ç", "c"},
|
||||
{"È", "E"}, {"Ê", "E"}, {"À", "A"}, {"Ç", "C"},
|
||||
|
||||
// German umlauts and Portuguese - common
|
||||
{"ä", "a"}, {"ö", "o"}, {"ü", "u"}, {"ã", "a"}, {"õ", "o"},
|
||||
{"Ä", "A"}, {"Ö", "O"}, {"Ü", "U"}, {"Ã", "A"}, {"Õ", "O"},
|
||||
}
|
||||
|
||||
for _, accent := range commonAccents {
|
||||
normalized = "REPLACE(" + normalized + ", '" + accent.from + "', '" + accent.to + "')"
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
// ItemNameAccentInsensitiveContains creates an accent-insensitive search predicate for the item name field.
|
||||
func ItemNameAccentInsensitiveContains(value string) predicate.Item {
|
||||
return AccentInsensitiveContains(item.FieldName, value)
|
||||
}
|
||||
|
||||
// ItemDescriptionAccentInsensitiveContains creates an accent-insensitive search predicate for the item description field.
|
||||
func ItemDescriptionAccentInsensitiveContains(value string) predicate.Item {
|
||||
return AccentInsensitiveContains(item.FieldDescription, value)
|
||||
}
|
||||
|
||||
// ItemSerialNumberAccentInsensitiveContains creates an accent-insensitive search predicate for the item serial number field.
|
||||
func ItemSerialNumberAccentInsensitiveContains(value string) predicate.Item {
|
||||
return AccentInsensitiveContains(item.FieldSerialNumber, value)
|
||||
}
|
||||
|
||||
// ItemModelNumberAccentInsensitiveContains creates an accent-insensitive search predicate for the item model number field.
|
||||
func ItemModelNumberAccentInsensitiveContains(value string) predicate.Item {
|
||||
return AccentInsensitiveContains(item.FieldModelNumber, value)
|
||||
}
|
||||
|
||||
// ItemManufacturerAccentInsensitiveContains creates an accent-insensitive search predicate for the item manufacturer field.
|
||||
func ItemManufacturerAccentInsensitiveContains(value string) predicate.Item {
|
||||
return AccentInsensitiveContains(item.FieldManufacturer, value)
|
||||
}
|
||||
|
||||
// ItemNotesAccentInsensitiveContains creates an accent-insensitive search predicate for the item notes field.
|
||||
func ItemNotesAccentInsensitiveContains(value string) predicate.Item {
|
||||
return AccentInsensitiveContains(item.FieldNotes, value)
|
||||
}
|
||||
147
backend/internal/data/ent/item_predicates_test.go
Normal file
147
backend/internal/data/ent/item_predicates_test.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package ent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuildGenericNormalizeExpression(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
field string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Simple field name",
|
||||
field: "name",
|
||||
expected: "name", // Should be wrapped in many REPLACE functions
|
||||
},
|
||||
{
|
||||
name: "Complex field name",
|
||||
field: "description",
|
||||
expected: "description",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := buildGenericNormalizeExpression(tt.field)
|
||||
|
||||
// Should contain the original field
|
||||
assert.Contains(t, result, tt.field)
|
||||
|
||||
// Should contain REPLACE functions for accent normalization
|
||||
assert.Contains(t, result, "REPLACE(")
|
||||
|
||||
// Should handle common accented characters
|
||||
assert.Contains(t, result, "'á'", "Should handle Spanish á")
|
||||
assert.Contains(t, result, "'é'", "Should handle Spanish é")
|
||||
assert.Contains(t, result, "'ñ'", "Should handle Spanish ñ")
|
||||
assert.Contains(t, result, "'ü'", "Should handle German ü")
|
||||
|
||||
// Should handle uppercase accents too
|
||||
assert.Contains(t, result, "'Á'", "Should handle uppercase Spanish Á")
|
||||
assert.Contains(t, result, "'É'", "Should handle uppercase Spanish É")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLiteNormalizeExpression(t *testing.T) {
|
||||
result := buildSQLiteNormalizeExpression("test_field")
|
||||
|
||||
// Should contain the field name and REPLACE functions
|
||||
assert.Contains(t, result, "test_field")
|
||||
assert.Contains(t, result, "REPLACE(")
|
||||
// Check for some specific accent replacements (order doesn't matter)
|
||||
assert.Contains(t, result, "'á'", "Should handle Spanish á")
|
||||
assert.Contains(t, result, "'ó'", "Should handle Spanish ó")
|
||||
}
|
||||
|
||||
func TestAccentInsensitivePredicateCreation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
field string
|
||||
searchValue string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "Normal search value",
|
||||
field: "name",
|
||||
searchValue: "electronica",
|
||||
description: "Should create predicate for normal search",
|
||||
},
|
||||
{
|
||||
name: "Accented search value",
|
||||
field: "description",
|
||||
searchValue: "electrónica",
|
||||
description: "Should create predicate for accented search",
|
||||
},
|
||||
{
|
||||
name: "Empty search value",
|
||||
field: "name",
|
||||
searchValue: "",
|
||||
description: "Should handle empty search gracefully",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
predicate := AccentInsensitiveContains(tt.field, tt.searchValue)
|
||||
assert.NotNil(t, predicate, tt.description)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecificItemPredicates(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
predicateFunc func(string) interface{}
|
||||
searchValue string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "ItemNameAccentInsensitiveContains",
|
||||
predicateFunc: func(val string) interface{} { return ItemNameAccentInsensitiveContains(val) },
|
||||
searchValue: "electronica",
|
||||
description: "Should create accent-insensitive name search predicate",
|
||||
},
|
||||
{
|
||||
name: "ItemDescriptionAccentInsensitiveContains",
|
||||
predicateFunc: func(val string) interface{} { return ItemDescriptionAccentInsensitiveContains(val) },
|
||||
searchValue: "descripcion",
|
||||
description: "Should create accent-insensitive description search predicate",
|
||||
},
|
||||
{
|
||||
name: "ItemManufacturerAccentInsensitiveContains",
|
||||
predicateFunc: func(val string) interface{} { return ItemManufacturerAccentInsensitiveContains(val) },
|
||||
searchValue: "compañia",
|
||||
description: "Should create accent-insensitive manufacturer search predicate",
|
||||
},
|
||||
{
|
||||
name: "ItemSerialNumberAccentInsensitiveContains",
|
||||
predicateFunc: func(val string) interface{} { return ItemSerialNumberAccentInsensitiveContains(val) },
|
||||
searchValue: "sn123",
|
||||
description: "Should create accent-insensitive serial number search predicate",
|
||||
},
|
||||
{
|
||||
name: "ItemModelNumberAccentInsensitiveContains",
|
||||
predicateFunc: func(val string) interface{} { return ItemModelNumberAccentInsensitiveContains(val) },
|
||||
searchValue: "model456",
|
||||
description: "Should create accent-insensitive model number search predicate",
|
||||
},
|
||||
{
|
||||
name: "ItemNotesAccentInsensitiveContains",
|
||||
predicateFunc: func(val string) interface{} { return ItemNotesAccentInsensitiveContains(val) },
|
||||
searchValue: "notas importantes",
|
||||
description: "Should create accent-insensitive notes search predicate",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
predicate := tt.predicateFunc(tt.searchValue)
|
||||
assert.NotNil(t, predicate, tt.description)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package migrations
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -17,15 +19,16 @@ var sqliteFiles embed.FS
|
||||
// migration files in the binary at build time. The function takes a string
|
||||
// parameter "dialect" which specifies the SQL dialect to use. It returns an
|
||||
// embedded file system containing the migration files for the specified dialect.
|
||||
func Migrations(dialect string) embed.FS {
|
||||
func Migrations(dialect string) (embed.FS, error) {
|
||||
switch dialect {
|
||||
case "postgres":
|
||||
return postgresFiles
|
||||
return postgresFiles, nil
|
||||
case "sqlite3":
|
||||
return sqliteFiles
|
||||
return sqliteFiles, nil
|
||||
default:
|
||||
log.Fatal().Str("dialect", dialect).Msg("unknown sql dialect")
|
||||
log.Error().Str("dialect", dialect).Msg("unknown sql dialect")
|
||||
return embed.FS{}, fmt.Errorf("unknown sql dialect: %s", dialect)
|
||||
}
|
||||
// This should never get hit, but just in case
|
||||
return sqliteFiles
|
||||
return sqliteFiles, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
-- +goose Up
|
||||
-- GENERATED with 20250706190000_generate_migration.py
|
||||
-- Migrating auth_tokens/created_at
|
||||
update auth_tokens set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update auth_tokens set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating auth_tokens/updated_at
|
||||
update auth_tokens set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update auth_tokens set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating auth_tokens/expires_at
|
||||
update auth_tokens set expires_at = substr(expires_at,1, instr(expires_at, ' +')-1) || substr(expires_at, instr(expires_at, ' +')+1,3) || ':' || substr(expires_at, instr(expires_at, ' +')+4,2) where expires_at like '% +%';
|
||||
update auth_tokens set expires_at = substr(expires_at,1, instr(expires_at, ' -')-1) || substr(expires_at, instr(expires_at, ' -')+1,3) || ':' || substr(expires_at, instr(expires_at, ' -')+4,2) where expires_at like '% -%';
|
||||
|
||||
-- Migrating groups/created_at
|
||||
update groups set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update groups set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating groups/updated_at
|
||||
update groups set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update groups set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating group_invitation_tokens/created_at
|
||||
update group_invitation_tokens set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update group_invitation_tokens set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating group_invitation_tokens/updated_at
|
||||
update group_invitation_tokens set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update group_invitation_tokens set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating group_invitation_tokens/expires_at
|
||||
update group_invitation_tokens set expires_at = substr(expires_at,1, instr(expires_at, ' +')-1) || substr(expires_at, instr(expires_at, ' +')+1,3) || ':' || substr(expires_at, instr(expires_at, ' +')+4,2) where expires_at like '% +%';
|
||||
update group_invitation_tokens set expires_at = substr(expires_at,1, instr(expires_at, ' -')-1) || substr(expires_at, instr(expires_at, ' -')+1,3) || ':' || substr(expires_at, instr(expires_at, ' -')+4,2) where expires_at like '% -%';
|
||||
|
||||
-- Migrating item_fields/created_at
|
||||
update item_fields set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update item_fields set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating item_fields/updated_at
|
||||
update item_fields set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update item_fields set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating item_fields/time_value
|
||||
update item_fields set time_value = substr(time_value,1, instr(time_value, ' +')-1) || substr(time_value, instr(time_value, ' +')+1,3) || ':' || substr(time_value, instr(time_value, ' +')+4,2) where time_value like '% +%';
|
||||
update item_fields set time_value = substr(time_value,1, instr(time_value, ' -')-1) || substr(time_value, instr(time_value, ' -')+1,3) || ':' || substr(time_value, instr(time_value, ' -')+4,2) where time_value like '% -%';
|
||||
|
||||
-- Migrating labels/created_at
|
||||
update labels set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update labels set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating labels/updated_at
|
||||
update labels set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update labels set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating locations/created_at
|
||||
update locations set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update locations set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating locations/updated_at
|
||||
update locations set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update locations set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating maintenance_entries/created_at
|
||||
update maintenance_entries set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update maintenance_entries set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating maintenance_entries/updated_at
|
||||
update maintenance_entries set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update maintenance_entries set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating maintenance_entries/date
|
||||
update maintenance_entries set date = substr(date,1, instr(date, ' +')-1) || substr(date, instr(date, ' +')+1,3) || ':' || substr(date, instr(date, ' +')+4,2) where date like '% +%';
|
||||
update maintenance_entries set date = substr(date,1, instr(date, ' -')-1) || substr(date, instr(date, ' -')+1,3) || ':' || substr(date, instr(date, ' -')+4,2) where date like '% -%';
|
||||
|
||||
-- Migrating maintenance_entries/scheduled_date
|
||||
update maintenance_entries set scheduled_date = substr(scheduled_date,1, instr(scheduled_date, ' +')-1) || substr(scheduled_date, instr(scheduled_date, ' +')+1,3) || ':' || substr(scheduled_date, instr(scheduled_date, ' +')+4,2) where scheduled_date like '% +%';
|
||||
update maintenance_entries set scheduled_date = substr(scheduled_date,1, instr(scheduled_date, ' -')-1) || substr(scheduled_date, instr(scheduled_date, ' -')+1,3) || ':' || substr(scheduled_date, instr(scheduled_date, ' -')+4,2) where scheduled_date like '% -%';
|
||||
|
||||
-- Migrating notifiers/created_at
|
||||
update notifiers set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update notifiers set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating notifiers/updated_at
|
||||
update notifiers set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update notifiers set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating users/created_at
|
||||
update users set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update users set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating users/updated_at
|
||||
update users set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update users set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating users/activated_on
|
||||
update users set activated_on = substr(activated_on,1, instr(activated_on, ' +')-1) || substr(activated_on, instr(activated_on, ' +')+1,3) || ':' || substr(activated_on, instr(activated_on, ' +')+4,2) where activated_on like '% +%';
|
||||
update users set activated_on = substr(activated_on,1, instr(activated_on, ' -')-1) || substr(activated_on, instr(activated_on, ' -')+1,3) || ':' || substr(activated_on, instr(activated_on, ' -')+4,2) where activated_on like '% -%';
|
||||
|
||||
-- Migrating items/created_at
|
||||
update items set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update items set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating items/updated_at
|
||||
update items set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update items set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating items/warranty_expires
|
||||
update items set warranty_expires = substr(warranty_expires,1, instr(warranty_expires, ' +')-1) || substr(warranty_expires, instr(warranty_expires, ' +')+1,3) || ':' || substr(warranty_expires, instr(warranty_expires, ' +')+4,2) where warranty_expires like '% +%';
|
||||
update items set warranty_expires = substr(warranty_expires,1, instr(warranty_expires, ' -')-1) || substr(warranty_expires, instr(warranty_expires, ' -')+1,3) || ':' || substr(warranty_expires, instr(warranty_expires, ' -')+4,2) where warranty_expires like '% -%';
|
||||
|
||||
-- Migrating items/purchase_time
|
||||
update items set purchase_time = substr(purchase_time,1, instr(purchase_time, ' +')-1) || substr(purchase_time, instr(purchase_time, ' +')+1,3) || ':' || substr(purchase_time, instr(purchase_time, ' +')+4,2) where purchase_time like '% +%';
|
||||
update items set purchase_time = substr(purchase_time,1, instr(purchase_time, ' -')-1) || substr(purchase_time, instr(purchase_time, ' -')+1,3) || ':' || substr(purchase_time, instr(purchase_time, ' -')+4,2) where purchase_time like '% -%';
|
||||
|
||||
-- Migrating items/sold_time
|
||||
update items set sold_time = substr(sold_time,1, instr(sold_time, ' +')-1) || substr(sold_time, instr(sold_time, ' +')+1,3) || ':' || substr(sold_time, instr(sold_time, ' +')+4,2) where sold_time like '% +%';
|
||||
update items set sold_time = substr(sold_time,1, instr(sold_time, ' -')-1) || substr(sold_time, instr(sold_time, ' -')+1,3) || ':' || substr(sold_time, instr(sold_time, ' -')+4,2) where sold_time like '% -%';
|
||||
|
||||
-- Migrating attachments/created_at
|
||||
update attachments set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update attachments set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating attachments/updated_at
|
||||
update attachments set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update attachments set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
|
||||
# Extract fields with
|
||||
""" WITH tables AS (
|
||||
SELECT name AS table_name
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table'
|
||||
AND name NOT LIKE 'sqlite_%'
|
||||
)
|
||||
|
||||
SELECT
|
||||
'["' || t.table_name || '", "' || c.name || '"],' AS table_column
|
||||
FROM tables t
|
||||
JOIN pragma_table_info(t.table_name) c
|
||||
WHERE c.name like'%date%'; """
|
||||
|
||||
fields = [["auth_tokens", "created_at"],
|
||||
["auth_tokens", "updated_at"],
|
||||
["auth_tokens", "expires_at"],
|
||||
["groups", "created_at"],
|
||||
["groups", "updated_at"],
|
||||
["group_invitation_tokens", "created_at"],
|
||||
["group_invitation_tokens", "updated_at"],
|
||||
["group_invitation_tokens", "expires_at"],
|
||||
["item_fields", "created_at"],
|
||||
["item_fields", "updated_at"],
|
||||
["item_fields", "time_value"],
|
||||
["labels", "created_at"],
|
||||
["labels", "updated_at"],
|
||||
["locations", "created_at"],
|
||||
["locations", "updated_at"],
|
||||
["maintenance_entries", "created_at"],
|
||||
["maintenance_entries", "updated_at"],
|
||||
["maintenance_entries", "date"],
|
||||
["maintenance_entries", "scheduled_date"],
|
||||
["notifiers", "created_at"],
|
||||
["notifiers", "updated_at"],
|
||||
["users", "created_at"],
|
||||
["users", "updated_at"],
|
||||
["users", "activated_on"],
|
||||
["items", "created_at"],
|
||||
["items", "updated_at"],
|
||||
["items", "warranty_expires"],
|
||||
["items", "purchase_time"],
|
||||
["items", "sold_time"],
|
||||
["attachments", "created_at"],
|
||||
["attachments", "updated_at"]]
|
||||
|
||||
|
||||
def generate_migration(table_name, field_name):
|
||||
return f"""update {table_name} set {field_name} = substr({field_name},1, instr({field_name}, ' +')-1) || substr({field_name}, instr({field_name}, ' +')+1,3) || ':' || substr({field_name}, instr({field_name}, ' +')+4,2) where {field_name} like '% +%';\n""" + \
|
||||
f"""update {table_name} set {field_name} = substr({field_name},1, instr({field_name}, ' -')-1) || substr({field_name}, instr({field_name}, ' -')+1,3) || ':' || substr({field_name}, instr({field_name}, ' -')+4,2) where {field_name} like '% -%';"""
|
||||
|
||||
|
||||
print("-- +goose Up")
|
||||
print(f"-- GENERATED with {os.path.basename(__file__)}")
|
||||
for table, column in fields:
|
||||
print(f"-- Migrating {table}/{column}")
|
||||
print(generate_migration(table, column))
|
||||
print()
|
||||
@@ -5,25 +5,27 @@ import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/group"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
||||
"github.com/sysadminsmedia/homebox/backend/pkgs/utils"
|
||||
"github.com/zeebo/blake3"
|
||||
|
||||
"github.com/gen2brain/avif"
|
||||
"github.com/gen2brain/heic"
|
||||
"github.com/gen2brain/jpegxl"
|
||||
"github.com/gen2brain/webp"
|
||||
"golang.org/x/image/draw"
|
||||
"image"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evanoberholster/imagemeta"
|
||||
"github.com/gen2brain/avif"
|
||||
"github.com/gen2brain/heic"
|
||||
"github.com/gen2brain/jpegxl"
|
||||
"github.com/gen2brain/webp"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/group"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
||||
"github.com/sysadminsmedia/homebox/backend/pkgs/utils"
|
||||
"github.com/zeebo/blake3"
|
||||
"golang.org/x/image/draw"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
|
||||
@@ -100,13 +102,30 @@ func (r *AttachmentRepo) path(gid uuid.UUID, hash string) string {
|
||||
}
|
||||
|
||||
func (r *AttachmentRepo) GetConnString() string {
|
||||
// Handle the default case for file storage
|
||||
// which is file:///./ meaning relative to the current working directory
|
||||
if strings.HasPrefix(r.storage.ConnString, "file:///./") {
|
||||
dir, err := filepath.Abs(strings.TrimPrefix(r.storage.ConnString, "file:///./"))
|
||||
if runtime.GOOS == "windows" {
|
||||
dir = fmt.Sprintf("/%s", dir)
|
||||
}
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to get absolute path for attachment directory")
|
||||
return r.storage.ConnString
|
||||
}
|
||||
return fmt.Sprintf("file://%s?no_tmp_dir=true", dir)
|
||||
return strings.ReplaceAll(fmt.Sprintf("file://%s?no_tmp_dir=true", dir), "\\", "/")
|
||||
} else if strings.HasPrefix(r.storage.ConnString, "file://") {
|
||||
// Handle the case for file storage with an absolute path
|
||||
// Convert Windows paths to a format compatible with fileblob
|
||||
// e.g. file:///C:/path/to/file becomes file:///C/path
|
||||
dir := strings.TrimPrefix(strings.ReplaceAll(r.storage.ConnString, "\\", "/"), "file://")
|
||||
if runtime.GOOS == "windows" {
|
||||
// Remove the colon from the drive letter (in case the user adds it)
|
||||
dir = strings.ReplaceAll(dir, ":", "")
|
||||
// Ensure the path starts with a slash for Windows compatibility
|
||||
dir = fmt.Sprintf("/%s", dir)
|
||||
}
|
||||
return fmt.Sprintf("file://%s", dir)
|
||||
}
|
||||
return r.storage.ConnString
|
||||
}
|
||||
@@ -256,16 +275,46 @@ func (r *AttachmentRepo) Create(ctx context.Context, itemID uuid.UUID, doc ItemC
|
||||
return attachmentDb, nil
|
||||
}
|
||||
|
||||
func (r *AttachmentRepo) Get(ctx context.Context, id uuid.UUID) (*ent.Attachment, error) {
|
||||
return r.db.Attachment.
|
||||
Query().
|
||||
Where(attachment.ID(id)).
|
||||
WithItem().
|
||||
WithThumbnail().
|
||||
Only(ctx)
|
||||
func (r *AttachmentRepo) Get(ctx context.Context, gid uuid.UUID, id uuid.UUID) (*ent.Attachment, error) {
|
||||
first, err := r.db.Attachment.Query().Where(attachment.ID(id)).Only(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if first.Type == attachment.TypeThumbnail {
|
||||
// If the attachment is a thumbnail, get the parent attachment and check if it belongs to the specified group
|
||||
return r.db.Attachment.
|
||||
Query().
|
||||
Where(attachment.ID(id),
|
||||
attachment.HasThumbnailWith(attachment.HasItemWith(item.HasGroupWith(group.ID(gid)))),
|
||||
).
|
||||
WithItem().
|
||||
WithThumbnail().
|
||||
Only(ctx)
|
||||
} else {
|
||||
// For regular attachments, check if the attachment's item belongs to the specified group
|
||||
return r.db.Attachment.
|
||||
Query().
|
||||
Where(attachment.ID(id),
|
||||
attachment.HasItemWith(item.HasGroupWith(group.ID(gid))),
|
||||
).
|
||||
WithItem().
|
||||
WithThumbnail().
|
||||
Only(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *AttachmentRepo) Update(ctx context.Context, id uuid.UUID, data *ItemAttachmentUpdate) (*ent.Attachment, error) {
|
||||
func (r *AttachmentRepo) Update(ctx context.Context, gid uuid.UUID, id uuid.UUID, data *ItemAttachmentUpdate) (*ent.Attachment, error) {
|
||||
// Validate that the attachment belongs to the specified group
|
||||
_, err := r.db.Attachment.Query().
|
||||
Where(
|
||||
attachment.ID(id),
|
||||
attachment.HasItemWith(item.HasGroupWith(group.ID(gid))),
|
||||
).
|
||||
Only(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: execute within Tx
|
||||
typ := attachment.Type(data.Type)
|
||||
|
||||
@@ -289,25 +338,34 @@ func (r *AttachmentRepo) Update(ctx context.Context, id uuid.UUID, data *ItemAtt
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure all other attachments are not primary
|
||||
err = r.db.Attachment.Update().
|
||||
Where(
|
||||
attachment.HasItemWith(item.ID(attachmentItem.ID)),
|
||||
attachment.IDNEQ(updatedAttachment.ID),
|
||||
).
|
||||
SetPrimary(false).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Only remove primary status from other photo attachments when setting a new photo as primary
|
||||
if typ == attachment.TypePhoto && data.Primary {
|
||||
err = r.db.Attachment.Update().
|
||||
Where(
|
||||
attachment.HasItemWith(item.ID(attachmentItem.ID)),
|
||||
attachment.IDNEQ(updatedAttachment.ID),
|
||||
attachment.TypeEQ(attachment.TypePhoto),
|
||||
).
|
||||
SetPrimary(false).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return r.Get(ctx, updatedAttachment.ID)
|
||||
return r.Get(ctx, gid, updatedAttachment.ID)
|
||||
}
|
||||
|
||||
func (r *AttachmentRepo) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
doc, error := r.db.Attachment.Get(ctx, id)
|
||||
if error != nil {
|
||||
return error
|
||||
func (r *AttachmentRepo) Delete(ctx context.Context, gid uuid.UUID, itemId uuid.UUID, id uuid.UUID) error {
|
||||
// Validate that the attachment belongs to the specified group
|
||||
doc, err := r.db.Attachment.Query().
|
||||
Where(
|
||||
attachment.ID(id),
|
||||
attachment.HasItemWith(item.HasGroupWith(group.ID(gid))),
|
||||
).
|
||||
Only(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
all, err := r.db.Attachment.Query().Where(attachment.Path(doc.Path)).All(ctx)
|
||||
@@ -358,7 +416,18 @@ func (r *AttachmentRepo) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return r.db.Attachment.DeleteOneID(id).Exec(ctx)
|
||||
}
|
||||
|
||||
func (r *AttachmentRepo) Rename(ctx context.Context, id uuid.UUID, title string) (*ent.Attachment, error) {
|
||||
func (r *AttachmentRepo) Rename(ctx context.Context, gid uuid.UUID, id uuid.UUID, title string) (*ent.Attachment, error) {
|
||||
// Validate that the attachment belongs to the specified group
|
||||
_, err := r.db.Attachment.Query().
|
||||
Where(
|
||||
attachment.ID(id),
|
||||
attachment.HasItemWith(item.HasGroupWith(group.ID(gid))),
|
||||
).
|
||||
Only(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.db.Attachment.UpdateOneID(id).SetTitle(title).Save(ctx)
|
||||
}
|
||||
|
||||
@@ -455,10 +524,13 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
|
||||
contentType := http.DetectContentType(contentBytes[:min(512, len(contentBytes))])
|
||||
|
||||
if contentType == "application/octet-stream" {
|
||||
if strings.HasSuffix(title, ".heic") || strings.HasSuffix(title, ".heif") {
|
||||
switch {
|
||||
case strings.HasSuffix(title, ".heic") || strings.HasSuffix(title, ".heif"):
|
||||
contentType = "image/heic"
|
||||
} else if strings.HasSuffix(title, ".avif") {
|
||||
case strings.HasSuffix(title, ".avif"):
|
||||
contentType = "image/avif"
|
||||
case strings.HasSuffix(title, ".jxl"):
|
||||
contentType = "image/jxl"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,10 +547,18 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
|
||||
}
|
||||
return err
|
||||
}
|
||||
dst := image.NewRGBA(image.Rect(0, 0, r.thumbnail.Width, r.thumbnail.Height))
|
||||
draw.ApproxBiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
|
||||
buf := new(bytes.Buffer)
|
||||
err = webp.Encode(buf, dst, webp.Options{Quality: 80, Lossless: false})
|
||||
log.Debug().Msg("reading original file orientation")
|
||||
imageMeta, err := imagemeta.Decode(bytes.NewReader(contentBytes))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to decode original file content")
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
orientation := uint16(imageMeta.Orientation)
|
||||
thumbnailPath, err := r.processThumbnailFromImage(ctx, groupId, img, title, orientation)
|
||||
if err != nil {
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
@@ -486,22 +566,7 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
|
||||
}
|
||||
return err
|
||||
}
|
||||
contentBytes := buf.Bytes()
|
||||
log.Debug().Msg("uploading thumbnail file")
|
||||
thumbnailFile, err := r.UploadFile(ctx, tx.Group.GetX(ctx, groupId), ItemCreateAttachment{
|
||||
Title: fmt.Sprintf("%s-thumb", title),
|
||||
Content: bytes.NewReader(contentBytes),
|
||||
})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to upload thumbnail file")
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
log.Debug().Msg("setting thumbnail file path in attachment")
|
||||
att.SetPath(thumbnailFile)
|
||||
att.SetPath(thumbnailPath)
|
||||
case contentType == "image/webp":
|
||||
log.Debug().Msg("creating thumbnail for webp file")
|
||||
img, err := webp.Decode(bytes.NewReader(contentBytes))
|
||||
@@ -513,10 +578,18 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
|
||||
}
|
||||
return err
|
||||
}
|
||||
dst := image.NewRGBA(image.Rect(0, 0, r.thumbnail.Width, r.thumbnail.Height))
|
||||
draw.ApproxBiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
|
||||
buf := new(bytes.Buffer)
|
||||
err = webp.Encode(buf, dst, webp.Options{Quality: 80, Lossless: false})
|
||||
log.Debug().Msg("reading original file orientation")
|
||||
imageMeta, err := imagemeta.Decode(bytes.NewReader(contentBytes))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to decode original file content")
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
orientation := uint16(imageMeta.Orientation)
|
||||
thumbnailPath, err := r.processThumbnailFromImage(ctx, groupId, img, title, orientation)
|
||||
if err != nil {
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
@@ -524,22 +597,7 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
|
||||
}
|
||||
return err
|
||||
}
|
||||
contentBytes := buf.Bytes()
|
||||
log.Debug().Msg("uploading thumbnail file")
|
||||
thumbnailFile, err := r.UploadFile(ctx, tx.Group.GetX(ctx, groupId), ItemCreateAttachment{
|
||||
Title: fmt.Sprintf("%s-thumb", title),
|
||||
Content: bytes.NewReader(contentBytes),
|
||||
})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to upload thumbnail file")
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
log.Debug().Msg("setting thumbnail file path in attachment")
|
||||
att.SetPath(thumbnailFile)
|
||||
att.SetPath(thumbnailPath)
|
||||
case contentType == "image/avif":
|
||||
log.Debug().Msg("creating thumbnail for avif file")
|
||||
img, err := avif.Decode(bytes.NewReader(contentBytes))
|
||||
@@ -551,10 +609,7 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
|
||||
}
|
||||
return err
|
||||
}
|
||||
dst := image.NewRGBA(image.Rect(0, 0, r.thumbnail.Width, r.thumbnail.Height))
|
||||
draw.ApproxBiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
|
||||
buf := new(bytes.Buffer)
|
||||
err = webp.Encode(buf, dst, webp.Options{Quality: 80, Lossless: false})
|
||||
thumbnailPath, err := r.processThumbnailFromImage(ctx, groupId, img, title, uint16(1))
|
||||
if err != nil {
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
@@ -562,22 +617,7 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
|
||||
}
|
||||
return err
|
||||
}
|
||||
contentBytes := buf.Bytes()
|
||||
log.Debug().Msg("uploading thumbnail file")
|
||||
thumbnailFile, err := r.UploadFile(ctx, tx.Group.GetX(ctx, groupId), ItemCreateAttachment{
|
||||
Title: fmt.Sprintf("%s-thumb", title),
|
||||
Content: bytes.NewReader(contentBytes),
|
||||
})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to upload thumbnail file")
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
log.Debug().Msg("setting thumbnail file path in attachment")
|
||||
att.SetPath(thumbnailFile)
|
||||
att.SetPath(thumbnailPath)
|
||||
case contentType == "image/heic" || contentType == "image/heif":
|
||||
log.Debug().Msg("creating thumbnail for heic file")
|
||||
img, err := heic.Decode(bytes.NewReader(contentBytes))
|
||||
@@ -589,10 +629,18 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
|
||||
}
|
||||
return err
|
||||
}
|
||||
dst := image.NewRGBA(image.Rect(0, 0, r.thumbnail.Width, r.thumbnail.Height))
|
||||
draw.ApproxBiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
|
||||
buf := new(bytes.Buffer)
|
||||
err = webp.Encode(buf, dst, webp.Options{Quality: 80, Lossless: false})
|
||||
log.Debug().Msg("reading original file orientation")
|
||||
imageMeta, err := imagemeta.Decode(bytes.NewReader(contentBytes))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to decode original file content")
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
orientation := uint16(imageMeta.Orientation)
|
||||
thumbnailPath, err := r.processThumbnailFromImage(ctx, groupId, img, title, orientation)
|
||||
if err != nil {
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
@@ -600,22 +648,7 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
|
||||
}
|
||||
return err
|
||||
}
|
||||
contentBytes := buf.Bytes()
|
||||
log.Debug().Msg("uploading thumbnail file")
|
||||
thumbnailFile, err := r.UploadFile(ctx, tx.Group.GetX(ctx, groupId), ItemCreateAttachment{
|
||||
Title: fmt.Sprintf("%s-thumb", title),
|
||||
Content: bytes.NewReader(contentBytes),
|
||||
})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to upload thumbnail file")
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
log.Debug().Msg("setting thumbnail file path in attachment")
|
||||
att.SetPath(thumbnailFile)
|
||||
att.SetPath(thumbnailPath)
|
||||
case contentType == "image/jxl":
|
||||
log.Debug().Msg("creating thumbnail for jpegxl file")
|
||||
img, err := jpegxl.Decode(bytes.NewReader(contentBytes))
|
||||
@@ -627,10 +660,7 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
|
||||
}
|
||||
return err
|
||||
}
|
||||
dst := image.NewRGBA(image.Rect(0, 0, r.thumbnail.Width, r.thumbnail.Height))
|
||||
draw.ApproxBiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
|
||||
buf := new(bytes.Buffer)
|
||||
err = webp.Encode(buf, dst, webp.Options{Quality: 80, Lossless: false})
|
||||
thumbnailPath, err := r.processThumbnailFromImage(ctx, groupId, img, title, uint16(1))
|
||||
if err != nil {
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
@@ -638,22 +668,7 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
|
||||
}
|
||||
return err
|
||||
}
|
||||
contentBytes := buf.Bytes()
|
||||
log.Debug().Msg("uploading thumbnail file")
|
||||
thumbnailFile, err := r.UploadFile(ctx, tx.Group.GetX(ctx, groupId), ItemCreateAttachment{
|
||||
Title: fmt.Sprintf("%s-thumb", title),
|
||||
Content: bytes.NewReader(contentBytes),
|
||||
})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to upload thumbnail file")
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
log.Debug().Msg("setting thumbnail file path in attachment")
|
||||
att.SetPath(thumbnailFile)
|
||||
att.SetPath(thumbnailPath)
|
||||
default:
|
||||
return fmt.Errorf("file type %s is not supported for thumbnail creation or document thumnails disabled", title)
|
||||
}
|
||||
@@ -784,3 +799,73 @@ func isImageFile(mimetype string) bool {
|
||||
// Check file extension for image types
|
||||
return strings.Contains(mimetype, "image/jpeg") || strings.Contains(mimetype, "image/png") || strings.Contains(mimetype, "image/gif")
|
||||
}
|
||||
|
||||
// calculateThumbnailDimensions calculates new dimensions that preserve aspect ratio
|
||||
// while fitting within the configured maximum width and height
|
||||
func calculateThumbnailDimensions(origWidth, origHeight, maxWidth, maxHeight int) (int, int) {
|
||||
if origWidth <= maxWidth && origHeight <= maxHeight {
|
||||
return origWidth, origHeight
|
||||
}
|
||||
|
||||
// Calculate scaling factors for both dimensions
|
||||
scaleX := float64(maxWidth) / float64(origWidth)
|
||||
scaleY := float64(maxHeight) / float64(origHeight)
|
||||
|
||||
// Use the smaller scaling factor to ensure both dimensions fit
|
||||
scale := scaleX
|
||||
if scaleY < scaleX {
|
||||
scale = scaleY
|
||||
}
|
||||
|
||||
newWidth := int(float64(origWidth) * scale)
|
||||
newHeight := int(float64(origHeight) * scale)
|
||||
|
||||
// Ensure we don't get zero dimensions
|
||||
if newWidth < 1 {
|
||||
newWidth = 1
|
||||
}
|
||||
if newHeight < 1 {
|
||||
newHeight = 1
|
||||
}
|
||||
|
||||
return newWidth, newHeight
|
||||
}
|
||||
|
||||
// processThumbnailFromImage handles the common thumbnail processing logic after image decoding
|
||||
// Returns the thumbnail file path or an error
|
||||
func (r *AttachmentRepo) processThumbnailFromImage(ctx context.Context, groupId uuid.UUID, img image.Image, title string, orientation uint16) (string, error) {
|
||||
bounds := img.Bounds()
|
||||
// Apply EXIF orientation if needed
|
||||
if orientation > 1 {
|
||||
img = utils.ApplyOrientation(img, orientation)
|
||||
bounds = img.Bounds()
|
||||
}
|
||||
newWidth, newHeight := calculateThumbnailDimensions(bounds.Dx(), bounds.Dy(), r.thumbnail.Width, r.thumbnail.Height)
|
||||
dst := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
|
||||
draw.ApproxBiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
err := webp.Encode(buf, dst, webp.Options{Quality: 80, Lossless: false})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
contentBytes := buf.Bytes()
|
||||
log.Debug().Msg("uploading thumbnail file")
|
||||
|
||||
// Get the group for uploading the thumbnail
|
||||
group, err := r.db.Group.Get(ctx, groupId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
thumbnailFile, err := r.UploadFile(ctx, group, ItemCreateAttachment{
|
||||
Title: fmt.Sprintf("%s-thumb", title),
|
||||
Content: bytes.NewReader(contentBytes),
|
||||
})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to upload thumbnail file")
|
||||
return "", err
|
||||
}
|
||||
|
||||
return thumbnailFile, nil
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestAttachmentRepo_Create(t *testing.T) {
|
||||
ids := []uuid.UUID{item.ID}
|
||||
t.Cleanup(func() {
|
||||
for _, id := range ids {
|
||||
_ = tRepos.Attachments.Delete(context.Background(), id)
|
||||
_ = tRepos.Attachments.Delete(context.Background(), tGroup.ID, item.ID, id)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -69,7 +69,7 @@ func TestAttachmentRepo_Create(t *testing.T) {
|
||||
|
||||
assert.Equal(t, tt.want.Type, got.Type)
|
||||
|
||||
withItems, err := tRepos.Attachments.Get(tt.args.ctx, got.ID)
|
||||
withItems, err := tRepos.Attachments.Get(tt.args.ctx, tGroup.ID, got.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.args.itemID, withItems.Edges.Item.ID)
|
||||
|
||||
@@ -86,17 +86,17 @@ func useAttachments(t *testing.T, n int) []*ent.Attachment {
|
||||
ids := make([]uuid.UUID, 0, n)
|
||||
t.Cleanup(func() {
|
||||
for _, id := range ids {
|
||||
_ = tRepos.Attachments.Delete(context.Background(), id)
|
||||
_ = tRepos.Attachments.Delete(context.Background(), tGroup.ID, item.ID, id)
|
||||
}
|
||||
})
|
||||
|
||||
attachments := make([]*ent.Attachment, n)
|
||||
for i := 0; i < n; i++ {
|
||||
attachment, err := tRepos.Attachments.Create(context.Background(), item.ID, ItemCreateAttachment{Title: "Test", Content: strings.NewReader("Test String")}, attachment.TypePhoto, true)
|
||||
attach, err := tRepos.Attachments.Create(context.Background(), item.ID, ItemCreateAttachment{Title: "Test", Content: strings.NewReader("Test String")}, attachment.TypePhoto, true)
|
||||
require.NoError(t, err)
|
||||
attachments[i] = attachment
|
||||
attachments[i] = attach
|
||||
|
||||
ids = append(ids, attachment.ID)
|
||||
ids = append(ids, attach.ID)
|
||||
}
|
||||
|
||||
return attachments
|
||||
@@ -107,13 +107,13 @@ func TestAttachmentRepo_Update(t *testing.T) {
|
||||
|
||||
for _, typ := range []attachment.Type{"photo", "manual", "warranty", "attachment"} {
|
||||
t.Run(string(typ), func(t *testing.T) {
|
||||
_, err := tRepos.Attachments.Update(context.Background(), entity.ID, &ItemAttachmentUpdate{
|
||||
_, err := tRepos.Attachments.Update(context.Background(), tGroup.ID, entity.ID, &ItemAttachmentUpdate{
|
||||
Type: string(typ),
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := tRepos.Attachments.Get(context.Background(), entity.ID)
|
||||
updated, err := tRepos.Attachments.Get(context.Background(), tGroup.ID, entity.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, typ, updated.Type)
|
||||
})
|
||||
@@ -122,11 +122,12 @@ func TestAttachmentRepo_Update(t *testing.T) {
|
||||
|
||||
func TestAttachmentRepo_Delete(t *testing.T) {
|
||||
entity := useAttachments(t, 1)[0]
|
||||
item := useItems(t, 1)[0]
|
||||
|
||||
err := tRepos.Attachments.Delete(context.Background(), entity.ID)
|
||||
err := tRepos.Attachments.Delete(context.Background(), tGroup.ID, item.ID, entity.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = tRepos.Attachments.Get(context.Background(), entity.ID)
|
||||
_, err = tRepos.Attachments.Get(context.Background(), tGroup.ID, entity.ID)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -135,13 +136,13 @@ func TestAttachmentRepo_EnsureSinglePrimaryAttachment(t *testing.T) {
|
||||
attachments := useAttachments(t, 2)
|
||||
|
||||
setAndVerifyPrimary := func(primaryAttachmentID, nonPrimaryAttachmentID uuid.UUID) {
|
||||
primaryAttachment, err := tRepos.Attachments.Update(ctx, primaryAttachmentID, &ItemAttachmentUpdate{
|
||||
primaryAttachment, err := tRepos.Attachments.Update(ctx, tGroup.ID, primaryAttachmentID, &ItemAttachmentUpdate{
|
||||
Type: attachment.TypePhoto.String(),
|
||||
Primary: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
nonPrimaryAttachment, err := tRepos.Attachments.Get(ctx, nonPrimaryAttachmentID)
|
||||
nonPrimaryAttachment, err := tRepos.Attachments.Get(ctx, tGroup.ID, nonPrimaryAttachmentID)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, primaryAttachment.Primary)
|
||||
@@ -151,3 +152,132 @@ func TestAttachmentRepo_EnsureSinglePrimaryAttachment(t *testing.T) {
|
||||
setAndVerifyPrimary(attachments[0].ID, attachments[1].ID)
|
||||
setAndVerifyPrimary(attachments[1].ID, attachments[0].ID)
|
||||
}
|
||||
|
||||
func TestAttachmentRepo_UpdateNonPhotoDoesNotAffectPrimaryPhoto(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
item := useItems(t, 1)[0]
|
||||
|
||||
// Create a photo attachment that will be primary
|
||||
photoAttachment, err := tRepos.Attachments.Create(ctx, item.ID, ItemCreateAttachment{Title: "Test Photo", Content: strings.NewReader("Photo content")}, attachment.TypePhoto, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a manual attachment (non-photo)
|
||||
manualAttachment, err := tRepos.Attachments.Create(ctx, item.ID, ItemCreateAttachment{Title: "Test Manual", Content: strings.NewReader("Manual content")}, attachment.TypeManual, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Cleanup
|
||||
t.Cleanup(func() {
|
||||
_ = tRepos.Attachments.Delete(ctx, tGroup.ID, item.ID, photoAttachment.ID)
|
||||
_ = tRepos.Attachments.Delete(ctx, tGroup.ID, item.ID, manualAttachment.ID)
|
||||
})
|
||||
|
||||
// Verify photo is primary initially
|
||||
photoAttachment, err = tRepos.Attachments.Get(ctx, tGroup.ID, photoAttachment.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, photoAttachment.Primary)
|
||||
|
||||
// Update the manual attachment (this should NOT affect the photo's primary status)
|
||||
_, err = tRepos.Attachments.Update(ctx, tGroup.ID, manualAttachment.ID, &ItemAttachmentUpdate{
|
||||
Type: attachment.TypeManual.String(),
|
||||
Title: "Updated Manual",
|
||||
Primary: false, // This should have no effect since it's not a photo
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify photo is still primary after updating the manual
|
||||
photoAttachment, err = tRepos.Attachments.Get(ctx, tGroup.ID, photoAttachment.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, photoAttachment.Primary, "Photo attachment should remain primary after updating non-photo attachment")
|
||||
|
||||
// Verify manual attachment is not primary
|
||||
manualAttachment, err = tRepos.Attachments.Get(ctx, tGroup.ID, manualAttachment.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, manualAttachment.Primary)
|
||||
}
|
||||
|
||||
func TestAttachmentRepo_AddingPDFAfterPhotoKeepsPhotoAsPrimary(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
item := useItems(t, 1)[0]
|
||||
|
||||
// Step 1: Upload a photo first (this should become primary since it's the first photo)
|
||||
photoAttachment, err := tRepos.Attachments.Create(ctx, item.ID, ItemCreateAttachment{Title: "Item Photo", Content: strings.NewReader("Photo content")}, attachment.TypePhoto, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Cleanup
|
||||
t.Cleanup(func() {
|
||||
_ = tRepos.Attachments.Delete(ctx, tGroup.ID, item.ID, photoAttachment.ID)
|
||||
})
|
||||
|
||||
// Verify photo becomes primary automatically (since it's the first photo)
|
||||
photoAttachment, err = tRepos.Attachments.Get(ctx, tGroup.ID, photoAttachment.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, photoAttachment.Primary, "First photo should automatically become primary")
|
||||
|
||||
// Step 2: Add a PDF receipt (this should NOT affect the photo's primary status)
|
||||
pdfAttachment, err := tRepos.Attachments.Create(ctx, item.ID, ItemCreateAttachment{Title: "Receipt PDF", Content: strings.NewReader("PDF content")}, attachment.TypeReceipt, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add to cleanup
|
||||
t.Cleanup(func() {
|
||||
_ = tRepos.Attachments.Delete(ctx, tGroup.ID, item.ID, pdfAttachment.ID)
|
||||
})
|
||||
|
||||
// Step 3: Verify photo is still primary after adding PDF
|
||||
photoAttachment, err = tRepos.Attachments.Get(ctx, tGroup.ID, photoAttachment.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, photoAttachment.Primary, "Photo should remain primary after adding PDF attachment")
|
||||
|
||||
// Verify PDF is not primary
|
||||
pdfAttachment, err = tRepos.Attachments.Get(ctx, tGroup.ID, pdfAttachment.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, pdfAttachment.Primary)
|
||||
|
||||
// Step 4: Test the actual item summary mapping (this is what determines the card display)
|
||||
updatedItem, err := tRepos.Items.GetOne(ctx, item.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The item should have the photo's ID as the imageId
|
||||
assert.NotNil(t, updatedItem.ImageID, "Item should have an imageId")
|
||||
assert.Equal(t, photoAttachment.ID, *updatedItem.ImageID, "Item's imageId should match the photo attachment ID")
|
||||
}
|
||||
|
||||
func TestAttachmentRepo_SettingPhotoPrimaryStillWorks(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
item := useItems(t, 1)[0]
|
||||
|
||||
// Create two photo attachments
|
||||
photo1, err := tRepos.Attachments.Create(ctx, item.ID, ItemCreateAttachment{Title: "Photo 1", Content: strings.NewReader("Photo 1 content")}, attachment.TypePhoto, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
photo2, err := tRepos.Attachments.Create(ctx, item.ID, ItemCreateAttachment{Title: "Photo 2", Content: strings.NewReader("Photo 2 content")}, attachment.TypePhoto, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Cleanup
|
||||
t.Cleanup(func() {
|
||||
_ = tRepos.Attachments.Delete(ctx, tGroup.ID, item.ID, photo1.ID)
|
||||
_ = tRepos.Attachments.Delete(ctx, tGroup.ID, item.ID, photo2.ID)
|
||||
})
|
||||
|
||||
// First photo should be primary (since it was created first)
|
||||
photo1, err = tRepos.Attachments.Get(ctx, tGroup.ID, photo1.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, photo1.Primary)
|
||||
|
||||
photo2, err = tRepos.Attachments.Get(ctx, tGroup.ID, photo2.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, photo2.Primary)
|
||||
|
||||
// Now set photo2 as primary (this should work and remove primary from photo1)
|
||||
photo2, err = tRepos.Attachments.Update(ctx, tGroup.ID, photo2.ID, &ItemAttachmentUpdate{
|
||||
Type: attachment.TypePhoto.String(),
|
||||
Title: "Photo 2",
|
||||
Primary: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, photo2.Primary)
|
||||
|
||||
// Verify photo1 is no longer primary
|
||||
photo1, err = tRepos.Attachments.Get(ctx, tGroup.ID, photo1.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, photo1.Primary, "Photo 1 should no longer be primary after setting Photo 2 as primary")
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/core/services/reporting/eventbus"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/itemfield"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/label"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/location"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/maintenanceentry"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/predicate"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/types"
|
||||
)
|
||||
@@ -46,6 +48,13 @@ type (
|
||||
OrderBy string `json:"orderBy"`
|
||||
}
|
||||
|
||||
DuplicateOptions struct {
|
||||
CopyMaintenance bool `json:"copyMaintenance"`
|
||||
CopyAttachments bool `json:"copyAttachments"`
|
||||
CopyCustomFields bool `json:"copyCustomFields"`
|
||||
CopyPrefix string `json:"copyPrefix"`
|
||||
}
|
||||
|
||||
ItemField struct {
|
||||
ID uuid.UUID `json:"id,omitempty"`
|
||||
Type string `json:"type"`
|
||||
@@ -360,14 +369,25 @@ func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q Ite
|
||||
}
|
||||
|
||||
if q.Search != "" {
|
||||
// Use accent-insensitive search predicates that normalize both
|
||||
// the search query and database field values during comparison.
|
||||
// For queries without accents, the traditional search is more efficient.
|
||||
qb.Where(
|
||||
item.Or(
|
||||
// Regular case-insensitive search (fastest)
|
||||
item.NameContainsFold(q.Search),
|
||||
item.DescriptionContainsFold(q.Search),
|
||||
item.SerialNumberContainsFold(q.Search),
|
||||
item.ModelNumberContainsFold(q.Search),
|
||||
item.ManufacturerContainsFold(q.Search),
|
||||
item.NotesContainsFold(q.Search),
|
||||
// Accent-insensitive search using custom predicates
|
||||
ent.ItemNameAccentInsensitiveContains(q.Search),
|
||||
ent.ItemDescriptionAccentInsensitiveContains(q.Search),
|
||||
ent.ItemSerialNumberAccentInsensitiveContains(q.Search),
|
||||
ent.ItemModelNumberAccentInsensitiveContains(q.Search),
|
||||
ent.ItemManufacturerAccentInsensitiveContains(q.Search),
|
||||
ent.ItemNotesAccentInsensitiveContains(q.Search),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -993,3 +1013,164 @@ func (e *ItemsRepository) SetPrimaryPhotos(ctx context.Context, gid uuid.UUID) (
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
// Duplicate creates a copy of an item with configurable options for what data to copy.
|
||||
// The new item will have the next available asset ID and a customizable prefix in the name.
|
||||
func (e *ItemsRepository) Duplicate(ctx context.Context, gid, id uuid.UUID, options DuplicateOptions) (ItemOut, error) {
|
||||
tx, err := e.db.Tx(ctx)
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to rollback transaction during item duplication")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Get the original item with all its data
|
||||
originalItem, err := e.getOne(ctx, item.ID(id), item.HasGroupWith(group.ID(gid)))
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
|
||||
nextAssetID, err := e.GetHighestAssetID(ctx, gid)
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
nextAssetID++
|
||||
|
||||
// Set default copy prefix if not provided
|
||||
if options.CopyPrefix == "" {
|
||||
options.CopyPrefix = "Copy of "
|
||||
}
|
||||
|
||||
// Create the new item directly in the transaction
|
||||
newItemID := uuid.New()
|
||||
itemBuilder := tx.Item.Create().
|
||||
SetID(newItemID).
|
||||
SetName(options.CopyPrefix + originalItem.Name).
|
||||
SetDescription(originalItem.Description).
|
||||
SetQuantity(originalItem.Quantity).
|
||||
SetLocationID(originalItem.Location.ID).
|
||||
SetGroupID(gid).
|
||||
SetAssetID(int(nextAssetID)).
|
||||
SetSerialNumber(originalItem.SerialNumber).
|
||||
SetModelNumber(originalItem.ModelNumber).
|
||||
SetManufacturer(originalItem.Manufacturer).
|
||||
SetLifetimeWarranty(originalItem.LifetimeWarranty).
|
||||
SetWarrantyExpires(originalItem.WarrantyExpires.Time()).
|
||||
SetWarrantyDetails(originalItem.WarrantyDetails).
|
||||
SetPurchaseTime(originalItem.PurchaseTime.Time()).
|
||||
SetPurchaseFrom(originalItem.PurchaseFrom).
|
||||
SetPurchasePrice(originalItem.PurchasePrice).
|
||||
SetSoldTime(originalItem.SoldTime.Time()).
|
||||
SetSoldTo(originalItem.SoldTo).
|
||||
SetSoldPrice(originalItem.SoldPrice).
|
||||
SetSoldNotes(originalItem.SoldNotes).
|
||||
SetNotes(originalItem.Notes).
|
||||
SetInsured(originalItem.Insured).
|
||||
SetArchived(originalItem.Archived).
|
||||
SetSyncChildItemsLocations(originalItem.SyncChildItemsLocations)
|
||||
|
||||
if originalItem.Parent != nil {
|
||||
itemBuilder.SetParentID(originalItem.Parent.ID)
|
||||
}
|
||||
|
||||
// Add labels
|
||||
if len(originalItem.Labels) > 0 {
|
||||
labelIDs := make([]uuid.UUID, len(originalItem.Labels))
|
||||
for i, label := range originalItem.Labels {
|
||||
labelIDs[i] = label.ID
|
||||
}
|
||||
itemBuilder.AddLabelIDs(labelIDs...)
|
||||
}
|
||||
|
||||
_, err = itemBuilder.Save(ctx)
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
|
||||
// Copy custom fields if requested
|
||||
if options.CopyCustomFields {
|
||||
for _, field := range originalItem.Fields {
|
||||
_, err = tx.ItemField.Create().
|
||||
SetItemID(newItemID).
|
||||
SetType(itemfield.Type(field.Type)).
|
||||
SetName(field.Name).
|
||||
SetTextValue(field.TextValue).
|
||||
SetNumberValue(field.NumberValue).
|
||||
SetBooleanValue(field.BooleanValue).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("field_name", field.Name).Msg("failed to copy custom field during duplication")
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy attachments if requested
|
||||
if options.CopyAttachments {
|
||||
for _, att := range originalItem.Attachments {
|
||||
// Get the original attachment file
|
||||
originalAttachment, err := tx.Attachment.Query().
|
||||
Where(attachment.ID(att.ID)).
|
||||
Only(ctx)
|
||||
if err != nil {
|
||||
// Log error but continue to copy other attachments
|
||||
log.Warn().Err(err).Str("attachment_id", att.ID.String()).Msg("failed to find attachment during duplication")
|
||||
continue
|
||||
}
|
||||
|
||||
// Create a copy of the attachment with the same file path
|
||||
// Since files are stored with hash-based paths, this is safe
|
||||
_, err = tx.Attachment.Create().
|
||||
SetItemID(newItemID).
|
||||
SetType(originalAttachment.Type).
|
||||
SetTitle(originalAttachment.Title).
|
||||
SetPath(originalAttachment.Path).
|
||||
SetMimeType(originalAttachment.MimeType).
|
||||
SetPrimary(originalAttachment.Primary).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("original_attachment_id", att.ID.String()).Msg("failed to copy attachment during duplication")
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy maintenance entries if requested
|
||||
if options.CopyMaintenance {
|
||||
maintenanceEntries, err := tx.MaintenanceEntry.Query().
|
||||
Where(maintenanceentry.HasItemWith(item.ID(id))).
|
||||
All(ctx)
|
||||
if err == nil {
|
||||
for _, entry := range maintenanceEntries {
|
||||
_, err = tx.MaintenanceEntry.Create().
|
||||
SetItemID(newItemID).
|
||||
SetDate(entry.Date).
|
||||
SetScheduledDate(entry.ScheduledDate).
|
||||
SetName(entry.Name).
|
||||
SetDescription(entry.Description).
|
||||
SetCost(entry.Cost).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("maintenance_entry_id", entry.ID.String()).Msg("failed to copy maintenance entry during duplication")
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
committed = true
|
||||
|
||||
e.publishMutationEvent(gid)
|
||||
|
||||
// Get the final item with all copied data
|
||||
return e.GetOne(ctx, newItemID)
|
||||
}
|
||||
|
||||
213
backend/internal/data/repo/repo_items_search_test.go
Normal file
213
backend/internal/data/repo/repo_items_search_test.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sysadminsmedia/homebox/backend/pkgs/textutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestItemsRepository_AccentInsensitiveSearch(t *testing.T) {
|
||||
// Test cases for accent-insensitive search
|
||||
testCases := []struct {
|
||||
name string
|
||||
itemName string
|
||||
searchQuery string
|
||||
shouldMatch bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "Spanish accented item, search without accents",
|
||||
itemName: "electrónica",
|
||||
searchQuery: "electronica",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'electrónica' when searching for 'electronica'",
|
||||
},
|
||||
{
|
||||
name: "Spanish accented item, search with accents",
|
||||
itemName: "electrónica",
|
||||
searchQuery: "electrónica",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'electrónica' when searching for 'electrónica'",
|
||||
},
|
||||
{
|
||||
name: "Non-accented item, search with accents",
|
||||
itemName: "electronica",
|
||||
searchQuery: "electrónica",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'electronica' when searching for 'electrónica' (bidirectional search)",
|
||||
},
|
||||
{
|
||||
name: "Spanish item with tilde, search without accents",
|
||||
itemName: "café",
|
||||
searchQuery: "cafe",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'café' when searching for 'cafe'",
|
||||
},
|
||||
{
|
||||
name: "Spanish item without tilde, search with accents",
|
||||
itemName: "cafe",
|
||||
searchQuery: "café",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'cafe' when searching for 'café' (bidirectional)",
|
||||
},
|
||||
{
|
||||
name: "French accented item, search without accents",
|
||||
itemName: "pére",
|
||||
searchQuery: "pere",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'pére' when searching for 'pere'",
|
||||
},
|
||||
{
|
||||
name: "French: père without accent, search with accents",
|
||||
itemName: "pere",
|
||||
searchQuery: "père",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'pere' when searching for 'père' (bidirectional)",
|
||||
},
|
||||
{
|
||||
name: "Mixed case with accents",
|
||||
itemName: "Electrónica",
|
||||
searchQuery: "ELECTRONICA",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'Electrónica' when searching for 'ELECTRONICA' (case insensitive)",
|
||||
},
|
||||
{
|
||||
name: "Bidirectional: Non-accented item, search with different accents",
|
||||
itemName: "cafe",
|
||||
searchQuery: "café",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'cafe' when searching for 'café' (bidirectional)",
|
||||
},
|
||||
{
|
||||
name: "Bidirectional: Item with accent, search with different accent",
|
||||
itemName: "résumé",
|
||||
searchQuery: "resume",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'résumé' when searching for 'resume' (bidirectional)",
|
||||
},
|
||||
{
|
||||
name: "Bidirectional: Spanish ñ to n",
|
||||
itemName: "espanol",
|
||||
searchQuery: "español",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'espanol' when searching for 'español' (bidirectional ñ)",
|
||||
},
|
||||
{
|
||||
name: "French: français with accent, search without",
|
||||
itemName: "français",
|
||||
searchQuery: "francais",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'français' when searching for 'francais'",
|
||||
},
|
||||
{
|
||||
name: "French: français without accent, search with",
|
||||
itemName: "francais",
|
||||
searchQuery: "français",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'francais' when searching for 'français' (bidirectional)",
|
||||
},
|
||||
{
|
||||
name: "French: été with accent, search without",
|
||||
itemName: "été",
|
||||
searchQuery: "ete",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'été' when searching for 'ete'",
|
||||
},
|
||||
{
|
||||
name: "French: été without accent, search with",
|
||||
itemName: "ete",
|
||||
searchQuery: "été",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'ete' when searching for 'été' (bidirectional)",
|
||||
},
|
||||
{
|
||||
name: "French: hôtel with accent, search without",
|
||||
itemName: "hôtel",
|
||||
searchQuery: "hotel",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'hôtel' when searching for 'hotel'",
|
||||
},
|
||||
{
|
||||
name: "French: hôtel without accent, search with",
|
||||
itemName: "hotel",
|
||||
searchQuery: "hôtel",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'hotel' when searching for 'hôtel' (bidirectional)",
|
||||
},
|
||||
{
|
||||
name: "French: naïve with accent, search without",
|
||||
itemName: "naïve",
|
||||
searchQuery: "naive",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'naïve' when searching for 'naive'",
|
||||
},
|
||||
{
|
||||
name: "French: naïve without accent, search with",
|
||||
itemName: "naive",
|
||||
searchQuery: "naïve",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'naive' when searching for 'naïve' (bidirectional)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Test the normalization logic used in the repository
|
||||
normalizedSearch := textutils.NormalizeSearchQuery(tc.searchQuery)
|
||||
|
||||
// This simulates what happens in the repository
|
||||
// The original search would find exact matches (case-insensitive)
|
||||
// The normalized search would find accent-insensitive matches
|
||||
|
||||
// Test that our normalization works as expected
|
||||
if tc.shouldMatch {
|
||||
// If it should match, then either the original query should match
|
||||
// or the normalized query should match when applied to the stored data
|
||||
assert.NotEqual(t, "", normalizedSearch, "Normalized search should not be empty")
|
||||
|
||||
// The key insight is that we're searching with both the original and normalized queries
|
||||
// So "electrónica" will be found when searching for "electronica" because:
|
||||
// 1. Original search: "electronica" doesn't match "electrónica"
|
||||
// 2. Normalized search: "electronica" matches the normalized version
|
||||
t.Logf("✓ %s: Item '%s' should be found with search '%s' (normalized: '%s')",
|
||||
tc.description, tc.itemName, tc.searchQuery, normalizedSearch)
|
||||
} else {
|
||||
t.Logf("✗ %s: Item '%s' should NOT be found with search '%s' (normalized: '%s')",
|
||||
tc.description, tc.itemName, tc.searchQuery, normalizedSearch)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeSearchQueryIntegration(t *testing.T) {
|
||||
// Test that the normalization function works correctly
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"electrónica", "electronica"},
|
||||
{"café", "cafe"},
|
||||
{"ELECTRÓNICA", "electronica"},
|
||||
{"Café París", "cafe paris"},
|
||||
{"hello world", "hello world"},
|
||||
// French accented words
|
||||
{"père", "pere"},
|
||||
{"français", "francais"},
|
||||
{"été", "ete"},
|
||||
{"hôtel", "hotel"},
|
||||
{"naïve", "naive"},
|
||||
{"PÈRE", "pere"},
|
||||
{"FRANÇAIS", "francais"},
|
||||
{"ÉTÉ", "ete"},
|
||||
{"HÔTEL", "hotel"},
|
||||
{"NAÏVE", "naive"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
result := textutils.NormalizeSearchQuery(tc.input)
|
||||
assert.Equal(t, tc.expected, result, "Normalization should work correctly")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -20,14 +20,14 @@ type LabelRepository struct {
|
||||
type (
|
||||
LabelCreate struct {
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
Description string `json:"description" validate:"max=255"`
|
||||
Description string `json:"description" validate:"max=1000"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
LabelUpdate struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
Description string `json:"description" validate:"max=255"`
|
||||
Description string `json:"description" validate:"max=1000"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
|
||||
18
backend/internal/data/repo/repo_product_search.go
Normal file
18
backend/internal/data/repo/repo_product_search.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package repo
|
||||
|
||||
type BarcodeProduct struct {
|
||||
SearchEngineName string `json:"search_engine_name"`
|
||||
|
||||
// Identifications
|
||||
ModelNumber string `json:"modelNumber"`
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
|
||||
// Extras
|
||||
Country string `json:"notes"`
|
||||
Barcode string `json:"barcode"`
|
||||
|
||||
ImageURL string `json:"imageURL"`
|
||||
ImageBase64 string `json:"imageBase64"`
|
||||
|
||||
Item ItemCreate `json:"item"`
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var startTime = time.Now()
|
||||
|
||||
type Data struct {
|
||||
Domain string `json:"domain"`
|
||||
Name string `json:"name"`
|
||||
@@ -18,7 +20,7 @@ type Data struct {
|
||||
Props map[string]interface{} `json:"props"`
|
||||
}
|
||||
|
||||
func Send(version, buildInfo string) {
|
||||
func Send(version, buildInfo string) error {
|
||||
hostData, _ := host.Info()
|
||||
analytics := Data{
|
||||
Domain: "homebox.software",
|
||||
@@ -32,22 +34,23 @@ func Send(version, buildInfo string) {
|
||||
"platform_version": hostData.PlatformVersion,
|
||||
"kernel_arch": hostData.KernelArch,
|
||||
"virt_type": hostData.VirtualizationSystem,
|
||||
"uptime_min": time.Since(startTime).Minutes(),
|
||||
},
|
||||
}
|
||||
jsonBody, err := json.Marshal(analytics)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to marshal analytics data")
|
||||
return
|
||||
return err
|
||||
}
|
||||
bodyReader := bytes.NewReader(jsonBody)
|
||||
req, err := http.NewRequest("POST", "https://a.sysadmins.zone/api/event", bodyReader)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to create analytics request")
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "Homebox/"+version+"/"+buildInfo+" (https://homebox.software)")
|
||||
req.Header.Set("User-Agent", "Homebox/"+version+"/(https://homebox.software)")
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
@@ -56,7 +59,7 @@ func Send(version, buildInfo string) {
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to send analytics request")
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
@@ -65,4 +68,5 @@ func Send(version, buildInfo string) {
|
||||
log.Error().Err(err).Msg("failed to close response body")
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ type Config struct {
|
||||
Options Options `yaml:"options"`
|
||||
LabelMaker LabelMakerConf `yaml:"labelmaker"`
|
||||
Thumbnail Thumbnail `yaml:"thumbnail"`
|
||||
Barcode BarcodeAPIConf `yaml:"barcode"`
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
@@ -60,14 +61,20 @@ type WebConfig struct {
|
||||
}
|
||||
|
||||
type LabelMakerConf struct {
|
||||
Width int64 `yaml:"width" conf:"default:526"`
|
||||
Height int64 `yaml:"height" conf:"default:200"`
|
||||
Padding int64 `yaml:"padding" conf:"default:32"`
|
||||
Margin int64 `yaml:"margin" conf:"default:32"`
|
||||
FontSize float64 `yaml:"font_size" conf:"default:32.0"`
|
||||
PrintCommand *string `yaml:"string"`
|
||||
AdditionalInformation *string `yaml:"string"`
|
||||
DynamicLength bool `yaml:"bool" conf:"default:true"`
|
||||
Width int64 `yaml:"width" conf:"default:526"`
|
||||
Height int64 `yaml:"height" conf:"default:200"`
|
||||
Padding int64 `yaml:"padding" conf:"default:32"`
|
||||
Margin int64 `yaml:"margin" conf:"default:32"`
|
||||
FontSize float64 `yaml:"font_size" conf:"default:32.0"`
|
||||
PrintCommand *string `yaml:"string"`
|
||||
AdditionalInformation *string `yaml:"string"`
|
||||
DynamicLength bool `yaml:"bool" conf:"default:true"`
|
||||
LabelServiceUrl *string `yaml:"label_service_url"`
|
||||
LabelServiceTimeout *time.Duration `yaml:"label_service_timeout"`
|
||||
}
|
||||
|
||||
type BarcodeAPIConf struct {
|
||||
TokenBarcodespider string `yaml:"token_barcodespider"`
|
||||
}
|
||||
|
||||
// New parses the CLI/Config file and returns a Config struct. If the file argument is an empty string, the
|
||||
|
||||
@@ -17,7 +17,10 @@ type Database struct {
|
||||
Host string `yaml:"host"`
|
||||
Port string `yaml:"port"`
|
||||
Database string `yaml:"database"`
|
||||
SslMode string `yaml:"ssl_mode"`
|
||||
SslMode string `yaml:"ssl_mode" conf:"default:prefer"`
|
||||
SslRootCert string `yaml:"ssl_rootcert"`
|
||||
SslCert string `yaml:"ssl_cert"`
|
||||
SslKey string `yaml:"ssl_key"`
|
||||
SqlitePath string `yaml:"sqlite_path" conf:"default:./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite"`
|
||||
PubSubConnString string `yaml:"pubsub_conn_string" conf:"default:mem://{{ .Topic }}"`
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -138,11 +140,18 @@ func wrapText(text string, face font.Face, maxWidth int, maxHeight int, lineHeig
|
||||
return wrappedLines, ""
|
||||
}
|
||||
|
||||
func GenerateLabel(w io.Writer, params *GenerateParameters) error {
|
||||
func GenerateLabel(w io.Writer, params *GenerateParameters, cfg *config.Config) error {
|
||||
if err := params.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If LabelServiceUrl is configured, fetch the label from the URL instead of generating it
|
||||
if cfg != nil && cfg.LabelMaker.LabelServiceUrl != nil && *cfg.LabelMaker.LabelServiceUrl != "" {
|
||||
log.Printf("LabelServiceUrl configured: %s", *cfg.LabelMaker.LabelServiceUrl)
|
||||
|
||||
return fetchLabelFromURL(w, *cfg.LabelMaker.LabelServiceUrl, params, cfg)
|
||||
}
|
||||
|
||||
bodyText := params.DescriptionText
|
||||
if params.AdditionalInformation != nil {
|
||||
bodyText = bodyText + "\n" + *params.AdditionalInformation
|
||||
@@ -218,7 +227,7 @@ func GenerateLabel(w io.Writer, params *GenerateParameters) error {
|
||||
// Create the actual image with calculated height
|
||||
bounds := image.Rect(0, 0, params.Width, requiredHeight)
|
||||
img := image.NewRGBA(bounds)
|
||||
draw.Draw(img, bounds, &image.Uniform{color.White}, image.Point{}, draw.Src)
|
||||
draw.Draw(img, bounds, &image.Uniform{C: color.White}, image.Point{}, draw.Src)
|
||||
|
||||
// Draw QR code onto the image
|
||||
draw.Draw(img,
|
||||
@@ -279,6 +288,98 @@ func createContext(font *truetype.Font, size float64, img *image.RGBA, dpi float
|
||||
return c
|
||||
}
|
||||
|
||||
// fetchLabelFromURL fetches an image from the specified URL and writes it to the writer
|
||||
func fetchLabelFromURL(w io.Writer, serviceURL string, params *GenerateParameters, cfg *config.Config) error {
|
||||
// Parse the base URL
|
||||
baseURL, err := url.Parse(serviceURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse service URL %s: %w", serviceURL, err)
|
||||
}
|
||||
|
||||
// Build query parameters with the same attributes passed to print command
|
||||
query := url.Values{}
|
||||
query.Set("Width", fmt.Sprintf("%d", params.Width))
|
||||
query.Set("Height", fmt.Sprintf("%d", params.Height))
|
||||
query.Set("QrSize", fmt.Sprintf("%d", params.QrSize))
|
||||
query.Set("Margin", fmt.Sprintf("%d", params.Margin))
|
||||
query.Set("ComponentPadding", fmt.Sprintf("%d", params.ComponentPadding))
|
||||
query.Set("TitleText", params.TitleText)
|
||||
query.Set("TitleFontSize", fmt.Sprintf("%f", params.TitleFontSize))
|
||||
query.Set("DescriptionText", params.DescriptionText)
|
||||
query.Set("DescriptionFontSize", fmt.Sprintf("%f", params.DescriptionFontSize))
|
||||
query.Set("Dpi", fmt.Sprintf("%f", params.Dpi))
|
||||
query.Set("URL", params.URL)
|
||||
query.Set("DynamicLength", fmt.Sprintf("%t", params.DynamicLength))
|
||||
|
||||
// Add AdditionalInformation if it exists
|
||||
if params.AdditionalInformation != nil {
|
||||
query.Set("AdditionalInformation", *params.AdditionalInformation)
|
||||
}
|
||||
|
||||
// Set the query parameters
|
||||
baseURL.RawQuery = query.Encode()
|
||||
finalServiceURL := baseURL.String()
|
||||
|
||||
log.Printf("Fetching label from URL: %s", finalServiceURL)
|
||||
|
||||
// Use configured timeout or default to 30 seconds
|
||||
timeout := 30 * time.Second
|
||||
if cfg != nil && cfg.LabelMaker.LabelServiceTimeout != nil {
|
||||
timeout = *cfg.LabelMaker.LabelServiceTimeout
|
||||
}
|
||||
|
||||
// Create HTTP client with configurable timeout
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
// Create HTTP request with custom headers
|
||||
req, err := http.NewRequest("GET", finalServiceURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request for URL %s: %w", finalServiceURL, err)
|
||||
}
|
||||
|
||||
// Set custom headers
|
||||
req.Header.Set("User-Agent", "Homebox-LabelMaker/1.0")
|
||||
req.Header.Set("Accept", "image/*")
|
||||
|
||||
// Make HTTP request to the label service
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch label from URL %s: %w", finalServiceURL, err)
|
||||
}
|
||||
|
||||
// Check if the response status is OK
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("label service returned status %d for URL %s", resp.StatusCode, finalServiceURL)
|
||||
}
|
||||
|
||||
// Check if the response is an image
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(contentType, "image/") {
|
||||
return fmt.Errorf("label service returned invalid content type %s, expected image/*", contentType)
|
||||
}
|
||||
|
||||
// Set default max response size (10MB)
|
||||
maxResponseSize := int64(10 << 20)
|
||||
if cfg != nil {
|
||||
maxResponseSize = cfg.Web.MaxUploadSize << 20
|
||||
}
|
||||
limitedReader := io.LimitReader(resp.Body, maxResponseSize)
|
||||
|
||||
// Copy the response body to the writer
|
||||
_, err = io.Copy(w, limitedReader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write fetched label data: %w", err)
|
||||
}
|
||||
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
log.Printf("failed to close response body: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func PrintLabel(cfg *config.Config, params *GenerateParameters) error {
|
||||
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("label-%d.png", time.Now().UnixNano()))
|
||||
f, err := os.OpenFile(tmpFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
|
||||
@@ -292,7 +393,7 @@ func PrintLabel(cfg *config.Config, params *GenerateParameters) error {
|
||||
}
|
||||
}()
|
||||
|
||||
err = GenerateLabel(f, params)
|
||||
err = GenerateLabel(f, params, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -303,8 +404,27 @@ func PrintLabel(cfg *config.Config, params *GenerateParameters) error {
|
||||
|
||||
commandTemplate := template.Must(template.New("command").Parse(*cfg.LabelMaker.PrintCommand))
|
||||
builder := &strings.Builder{}
|
||||
additionalInformation := func() string {
|
||||
if params.AdditionalInformation != nil {
|
||||
return *params.AdditionalInformation
|
||||
}
|
||||
return ""
|
||||
}()
|
||||
if err := commandTemplate.Execute(builder, map[string]string{
|
||||
"FileName": f.Name(),
|
||||
"FileName": f.Name(),
|
||||
"Width": fmt.Sprintf("%d", params.Width),
|
||||
"Height": fmt.Sprintf("%d", params.Height),
|
||||
"QrSize": fmt.Sprintf("%d", params.QrSize),
|
||||
"Margin": fmt.Sprintf("%d", params.Margin),
|
||||
"ComponentPadding": fmt.Sprintf("%d", params.ComponentPadding),
|
||||
"TitleText": params.TitleText,
|
||||
"TitleFontSize": fmt.Sprintf("%f", params.TitleFontSize),
|
||||
"DescriptionText": params.DescriptionText,
|
||||
"DescriptionFontSize": fmt.Sprintf("%f", params.DescriptionFontSize),
|
||||
"AdditionalInformation": additionalInformation,
|
||||
"Dpi": fmt.Sprintf("%f", params.Dpi),
|
||||
"URL": params.URL,
|
||||
"DynamicLength": fmt.Sprintf("%t", params.DynamicLength),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
40
backend/pkgs/textutils/normalize.go
Normal file
40
backend/pkgs/textutils/normalize.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package textutils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/text/runes"
|
||||
"golang.org/x/text/transform"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
// RemoveAccents removes accents from text by normalizing Unicode characters
|
||||
// and removing diacritical marks. This allows for accent-insensitive search.
|
||||
//
|
||||
// Example:
|
||||
// - "electrónica" becomes "electronica"
|
||||
// - "café" becomes "cafe"
|
||||
// - "père" becomes "pere"
|
||||
func RemoveAccents(text string) string {
|
||||
// Create a transformer that:
|
||||
// 1. Normalizes to NFD (canonical decomposition)
|
||||
// 2. Removes diacritical marks (combining characters)
|
||||
// 3. Normalizes back to NFC (canonical composition)
|
||||
t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
|
||||
|
||||
result, _, err := transform.String(t, text)
|
||||
if err != nil {
|
||||
// If transformation fails, return the original text
|
||||
return text
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// NormalizeSearchQuery normalizes a search query for accent-insensitive matching.
|
||||
// This function removes accents and converts to lowercase for consistent search behavior.
|
||||
func NormalizeSearchQuery(query string) string {
|
||||
normalized := RemoveAccents(query)
|
||||
return strings.ToLower(normalized)
|
||||
}
|
||||
152
backend/pkgs/textutils/normalize_test.go
Normal file
152
backend/pkgs/textutils/normalize_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package textutils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRemoveAccents(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Spanish accented characters",
|
||||
input: "electrónica",
|
||||
expected: "electronica",
|
||||
},
|
||||
{
|
||||
name: "Spanish accented characters with tilde",
|
||||
input: "café",
|
||||
expected: "cafe",
|
||||
},
|
||||
{
|
||||
name: "French accented characters",
|
||||
input: "père",
|
||||
expected: "pere",
|
||||
},
|
||||
{
|
||||
name: "German umlauts",
|
||||
input: "Björk",
|
||||
expected: "Bjork",
|
||||
},
|
||||
{
|
||||
name: "Mixed accented characters",
|
||||
input: "résumé",
|
||||
expected: "resume",
|
||||
},
|
||||
{
|
||||
name: "Portuguese accented characters",
|
||||
input: "João",
|
||||
expected: "Joao",
|
||||
},
|
||||
{
|
||||
name: "No accents",
|
||||
input: "hello world",
|
||||
expected: "hello world",
|
||||
},
|
||||
{
|
||||
name: "Empty string",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "Numbers and symbols",
|
||||
input: "123!@#",
|
||||
expected: "123!@#",
|
||||
},
|
||||
{
|
||||
name: "Multiple accents in one word",
|
||||
input: "été",
|
||||
expected: "ete",
|
||||
},
|
||||
{
|
||||
name: "Complex Unicode characters",
|
||||
input: "français",
|
||||
expected: "francais",
|
||||
},
|
||||
{
|
||||
name: "Unicode diacritics",
|
||||
input: "naïve",
|
||||
expected: "naive",
|
||||
},
|
||||
{
|
||||
name: "Unicode combining characters",
|
||||
input: "e\u0301", // e with combining acute accent
|
||||
expected: "e",
|
||||
},
|
||||
{
|
||||
name: "Very long string with accents",
|
||||
input: strings.Repeat("café", 1000),
|
||||
expected: strings.Repeat("cafe", 1000),
|
||||
},
|
||||
{
|
||||
name: "All French accents",
|
||||
input: "àâäéèêëïîôöùûüÿç",
|
||||
expected: "aaaeeeeiioouuuyc",
|
||||
},
|
||||
{
|
||||
name: "All Spanish accents",
|
||||
input: "áéíóúñüÁÉÍÓÚÑÜ",
|
||||
expected: "aeiounuAEIOUNU",
|
||||
},
|
||||
{
|
||||
name: "All German umlauts",
|
||||
input: "äöüÄÖÜß",
|
||||
expected: "aouAOUß",
|
||||
},
|
||||
{
|
||||
name: "Mixed languages",
|
||||
input: "Français café España niño",
|
||||
expected: "Francais cafe Espana nino",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := RemoveAccents(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("RemoveAccents(%q) = %q, expected %q", tc.input, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeSearchQuery(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Uppercase with accents",
|
||||
input: "ELECTRÓNICA",
|
||||
expected: "electronica",
|
||||
},
|
||||
{
|
||||
name: "Mixed case with accents",
|
||||
input: "Electrónica",
|
||||
expected: "electronica",
|
||||
},
|
||||
{
|
||||
name: "Multiple words with accents",
|
||||
input: "Café París",
|
||||
expected: "cafe paris",
|
||||
},
|
||||
{
|
||||
name: "No accents mixed case",
|
||||
input: "Hello World",
|
||||
expected: "hello world",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := NormalizeSearchQuery(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("NormalizeSearchQuery(%q) = %q, expected %q", tc.input, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
86
backend/pkgs/utils/image.go
Normal file
86
backend/pkgs/utils/image.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package utils
|
||||
|
||||
import "image"
|
||||
|
||||
// flipHorizontal will flip the image horizontally. There is a limit of 10000 pixels in either dimension to prevent excessive memory usage.
|
||||
func flipHorizontal(img image.Image) image.Image {
|
||||
b := img.Bounds()
|
||||
if b.Dx() > 10000 || b.Dy() > 10000 {
|
||||
return img
|
||||
}
|
||||
dst := image.NewRGBA(b)
|
||||
for y := b.Min.Y; y < b.Max.Y; y++ {
|
||||
for x := b.Min.X; x < b.Max.X; x++ {
|
||||
dst.Set(b.Max.X-1-(x-b.Min.X), y, img.At(x, y))
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// flipVertical will flip the image vertically. There is a limit of 10000 pixels in either dimension to prevent excessive memory usage.
|
||||
func flipVertical(img image.Image) image.Image {
|
||||
b := img.Bounds()
|
||||
if b.Dx() > 10000 || b.Dy() > 10000 {
|
||||
return img
|
||||
}
|
||||
dst := image.NewRGBA(b)
|
||||
for y := b.Min.Y; y < b.Max.Y; y++ {
|
||||
for x := b.Min.X; x < b.Max.X; x++ {
|
||||
dst.Set(x, b.Max.Y-1-(y-b.Min.Y), img.At(x, y))
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// rotate90 will rotate the image 90 degrees clockwise. There is a limit of 10000 pixels in either dimension to prevent excessive memory usage.
|
||||
func rotate90(img image.Image) image.Image {
|
||||
b := img.Bounds()
|
||||
if b.Dx() > 10000 || b.Dy() > 10000 {
|
||||
return img
|
||||
}
|
||||
dst := image.NewRGBA(image.Rect(0, 0, b.Dy(), b.Dx()))
|
||||
for y := b.Min.Y; y < b.Max.Y; y++ {
|
||||
for x := b.Min.X; x < b.Max.X; x++ {
|
||||
dst.Set(b.Max.Y-1-y, x, img.At(x, y))
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func rotate180(img image.Image) image.Image {
|
||||
return rotate90(rotate90(img))
|
||||
}
|
||||
|
||||
func rotate270(img image.Image) image.Image {
|
||||
return rotate90(rotate180(img))
|
||||
}
|
||||
|
||||
// Applies EXIF orientation using only stdlib
|
||||
func ApplyOrientation(img image.Image, orientation uint16) image.Image {
|
||||
if img == nil {
|
||||
return nil
|
||||
}
|
||||
if orientation < 1 || orientation > 8 {
|
||||
return img // No orientation or invalid orientation
|
||||
}
|
||||
switch orientation {
|
||||
case 1:
|
||||
return img // No rotation needed
|
||||
case 2:
|
||||
return flipHorizontal(img)
|
||||
case 3:
|
||||
return rotate180(img)
|
||||
case 4:
|
||||
return flipVertical(img)
|
||||
case 5:
|
||||
return rotate90(flipHorizontal(img))
|
||||
case 6:
|
||||
return rotate90(img)
|
||||
case 7:
|
||||
return rotate270(flipHorizontal(img))
|
||||
case 8:
|
||||
return rotate270(img)
|
||||
default:
|
||||
return img
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ services:
|
||||
image: homebox
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile
|
||||
dockerfile: ./Dockerfile.hardened
|
||||
args:
|
||||
- COMMIT=head
|
||||
- BUILD_TIME=0001-01-01T00:00:00Z
|
||||
@@ -12,7 +12,6 @@ services:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
- linux/riscv64
|
||||
environment:
|
||||
- HBOX_DEBUG=true
|
||||
- HBOX_LOGGER_LEVEL=-1
|
||||
|
||||
@@ -941,6 +941,48 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/items/{id}/duplicate": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
],
|
||||
"summary": "Duplicate Item",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Item ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Duplicate Options",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.DuplicateOptions"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.ItemOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/items/{id}/maintenance": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -1809,6 +1851,41 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/products/search-from-barcode": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
],
|
||||
"summary": "Search EAN from Barcode",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "barcode to be searched",
|
||||
"name": "data",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.BarcodeProduct"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/qrcode": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -3061,6 +3138,54 @@
|
||||
"TypeTime"
|
||||
]
|
||||
},
|
||||
"repo.BarcodeProduct": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"barcode": {
|
||||
"type": "string"
|
||||
},
|
||||
"imageBase64": {
|
||||
"type": "string"
|
||||
},
|
||||
"imageURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"item": {
|
||||
"$ref": "#/definitions/repo.ItemCreate"
|
||||
},
|
||||
"manufacturer": {
|
||||
"type": "string"
|
||||
},
|
||||
"modelNumber": {
|
||||
"description": "Identifications",
|
||||
"type": "string"
|
||||
},
|
||||
"notes": {
|
||||
"description": "Extras",
|
||||
"type": "string"
|
||||
},
|
||||
"search_engine_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.DuplicateOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"copyAttachments": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"copyCustomFields": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"copyMaintenance": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"copyPrefix": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.Group": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3571,7 +3696,7 @@
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
"maxLength": 1000
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
|
||||
@@ -646,6 +646,38 @@ definitions:
|
||||
- TypeNumber
|
||||
- TypeBoolean
|
||||
- TypeTime
|
||||
repo.BarcodeProduct:
|
||||
properties:
|
||||
barcode:
|
||||
type: string
|
||||
imageBase64:
|
||||
type: string
|
||||
imageURL:
|
||||
type: string
|
||||
item:
|
||||
$ref: '#/definitions/repo.ItemCreate'
|
||||
manufacturer:
|
||||
type: string
|
||||
modelNumber:
|
||||
description: Identifications
|
||||
type: string
|
||||
notes:
|
||||
description: Extras
|
||||
type: string
|
||||
search_engine_name:
|
||||
type: string
|
||||
type: object
|
||||
repo.DuplicateOptions:
|
||||
properties:
|
||||
copyAttachments:
|
||||
type: boolean
|
||||
copyCustomFields:
|
||||
type: boolean
|
||||
copyMaintenance:
|
||||
type: boolean
|
||||
copyPrefix:
|
||||
type: string
|
||||
type: object
|
||||
repo.Group:
|
||||
properties:
|
||||
createdAt:
|
||||
@@ -991,7 +1023,7 @@ definitions:
|
||||
color:
|
||||
type: string
|
||||
description:
|
||||
maxLength: 255
|
||||
maxLength: 1000
|
||||
type: string
|
||||
name:
|
||||
maxLength: 255
|
||||
@@ -1947,6 +1979,32 @@ paths:
|
||||
summary: Update Item Attachment
|
||||
tags:
|
||||
- Items Attachments
|
||||
/v1/items/{id}/duplicate:
|
||||
post:
|
||||
parameters:
|
||||
- description: Item ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Duplicate Options
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/repo.DuplicateOptions'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
schema:
|
||||
$ref: '#/definitions/repo.ItemOut'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Duplicate Item
|
||||
tags:
|
||||
- Items
|
||||
/v1/items/{id}/maintenance:
|
||||
get:
|
||||
parameters:
|
||||
@@ -2543,6 +2601,27 @@ paths:
|
||||
summary: Test Notifier
|
||||
tags:
|
||||
- Notifiers
|
||||
/v1/products/search-from-barcode:
|
||||
get:
|
||||
parameters:
|
||||
- description: barcode to be searched
|
||||
in: query
|
||||
name: data
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/repo.BarcodeProduct'
|
||||
type: array
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Search EAN from Barcode
|
||||
tags:
|
||||
- Items
|
||||
/v1/qrcode:
|
||||
get:
|
||||
parameters:
|
||||
|
||||
@@ -11,7 +11,7 @@ aside: false
|
||||
|-----------------------------------------|----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| HBOX_MODE | `production` | application mode used for runtime behavior can be one of: `development`, `production` |
|
||||
| HBOX_WEB_PORT | 7745 | port to run the web server on, if you're using docker do not change this |
|
||||
| HBOX_WEB_HOST | | host to run the web server on, if you're using docker do not change this |
|
||||
| HBOX_WEB_HOST | | host to run the web server on, if you're using docker do not change this. see below for examples |
|
||||
| HBOX_OPTIONS_ALLOW_REGISTRATION | true | allow users to register themselves |
|
||||
| HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID | true | auto-increments the asset_id field for new items |
|
||||
| HBOX_OPTIONS_CURRENCY_CONFIG | | json configuration file containing additional currencie |
|
||||
@@ -35,10 +35,13 @@ aside: false
|
||||
| HBOX_DATABASE_SQLITE_PATH | ./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1 | sets the directory path for Sqlite |
|
||||
| HBOX_DATABASE_HOST | | sets the hostname for a postgres database |
|
||||
| HBOX_DATABASE_PORT | | sets the port for a postgres database |
|
||||
| HBOX_DATABASE_USERNAME | | sets the username for a postgres connection |
|
||||
| HBOX_DATABASE_PASSWORD | | sets the password for a postgres connection |
|
||||
| HBOX_DATABASE_USERNAME | | sets the username for a postgres connection (optional if using cert auth) |
|
||||
| HBOX_DATABASE_PASSWORD | | sets the password for a postgres connection (optional if using cert auth) |
|
||||
| HBOX_DATABASE_DATABASE | | sets the database for a postgres connection |
|
||||
| HBOX_DATABASE_SSL_MODE | | sets the sslmode for a postgres connection |
|
||||
| HBOX_DATABASE_SSL_CERT | | sets the sslcert for a postgres connection (should be a path) |
|
||||
| HBOX_DATABASE_SSL_KEY | | sets the sslkey for a postgres connection (should be a path) |
|
||||
| HBOX_DATABASE_SSL_ROOTCERT | | sets the sslrootcert for a postgres connection (should be a path) |
|
||||
| HBOX_OPTIONS_CHECK_GITHUB_RELEASE | true | check for new github releases |
|
||||
| HBOX_LABEL_MAKER_WIDTH | 526 | width for generated labels in pixels |
|
||||
| HBOX_LABEL_MAKER_HEIGHT | 200 | height for generated labels in pixels |
|
||||
@@ -51,6 +54,84 @@ aside: false
|
||||
| HBOX_THUMBNAIL_WIDTH | 500 | width for generated thumbnails in pixels |
|
||||
| HBOX_THUMBNAIL_HEIGHT | 500 | height for generated thumbnails in pixels |
|
||||
|
||||
### HBOX_WEB_HOST examples
|
||||
|
||||
| Value | Notes |
|
||||
|-----------------------------|------------------------------------------------------------|
|
||||
| 0.0.0.0 | Visible all interfaces (default behaviour) |
|
||||
| 127.0.0.1 | Only visible on same host |
|
||||
| 100.64.0.1 | Only visible on a specific interface (e.g., VPN in a VPS). |
|
||||
| unix?path=/run/homebox.sock | Listen on unix socket at specified path |
|
||||
| sysd?name=homebox.socket | Listen on systemd socket |
|
||||
|
||||
For unix and systemd socket address syntax and available options, see the [anyhttp address-syntax documentation](https://pkg.go.dev/go.balki.me/anyhttp#readme-address-syntax).
|
||||
|
||||
#### Private network example
|
||||
|
||||
Below example starts homebox in an isolated network. The process cannot make
|
||||
any external requests (including check for newer release) and thus more secure.
|
||||
|
||||
```bash
|
||||
❯ sudo systemd-run --property=PrivateNetwork=yes --uid $UID --pty --same-dir --wait --collect homebox --web-host "unix?path=/run/user/$UID/homebox.sock"
|
||||
Running as unit: run-p74482-i74483.service
|
||||
Press ^] three times within 1s to disconnect TTY.
|
||||
2025/07/11 22:33:29 goose: no migrations to run. current version: 20250706190000
|
||||
10:33PM INF ../../../go/src/app/app/api/handlers/v1/v1_ctrl_auth.go:98 > registering auth provider name=local
|
||||
10:33PM INF ../../../go/src/app/app/api/main.go:275 > Server is running on unix?path=/run/user/1000/homebox.sock
|
||||
10:33PM ERR ../../../go/src/app/app/api/main.go:403 > failed to get latest github release error="failed to make latest version request: Get \"https://api.github.com/repos/sysadminsmedia/homebox/releases/l
|
||||
atest\": dial tcp: lookup api.github.com on [::1]:53: read udp [::1]:50951->[::1]:53: read: connection refused"
|
||||
10:33PM INF ../../../go/src/app/internal/web/mid/logger.go:36 > request received method=GET path=/ rid=hname/PoXyRgt6ol-000001
|
||||
10:33PM INF ../../../go/src/app/internal/web/mid/logger.go:41 > request finished method=GET path=/ rid=hname/PoXyRgt6ol-000001 status=0
|
||||
```
|
||||
|
||||
#### Systemd socket example
|
||||
|
||||
In the example below, Homebox listens on a systemd socket securely so that only
|
||||
the webserver (Caddy) can access it. Other processes/containers on the host
|
||||
cannot connect to Homebox directly, bypassing the webserver.
|
||||
|
||||
File: homebox.socket
|
||||
```systemd
|
||||
# /usr/local/lib/systemd/system/homebox.socket
|
||||
[Unit]
|
||||
Description=Homebox socket
|
||||
|
||||
[Socket]
|
||||
ListenStream=/run/homebox.sock
|
||||
SocketGroup=caddy
|
||||
SocketMode=0660
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
```
|
||||
|
||||
File: homebox.service
|
||||
```systemd
|
||||
# /usr/local/lib/systemd/system/homebox.service
|
||||
[Unit]
|
||||
Description=Homebox
|
||||
After=network.target
|
||||
Documentation=https://homebox.software
|
||||
|
||||
[Service]
|
||||
DynamicUser=yes
|
||||
StateDirectory=homebox
|
||||
Environment=HBOX_WEB_HOST=sysd?name=homebox.socket
|
||||
WorkingDirectory=/var/lib/homebox
|
||||
|
||||
ExecStart=/usr/local/bin/homebox
|
||||
|
||||
NoNewPrivileges=yes
|
||||
CapabilityBoundingSet=
|
||||
RestrictNamespaces=true
|
||||
SystemCallFilter=@system-service
|
||||
```
|
||||
Usage:
|
||||
|
||||
```bash
|
||||
systemctl start homebox.socket
|
||||
```
|
||||
|
||||
::: warning Security Considerations
|
||||
For postgreSQL in production:
|
||||
|
||||
|
||||
4
docs/public/_headers
Normal file
4
docs/public/_headers
Normal file
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
X-Frame-Options: DENY
|
||||
X-Content-Type-Options: nosniff
|
||||
Content-Security-Policy: default-src 'self'; script-src 'report-sample' 'unsafe-inline' 'self' https://a.sysadmins.zone/js/embed.host.js https://static.cloudflareinsights.com/beacon.min.js/vcd15cbe7772f49c399c6a5babf22c1241717689176015 https://unpkg.com/@stoplight/elements/web-components.min.js; style-src 'report-sample' 'unsafe-inline' 'self' https://unpkg.com; object-src 'none'; base-uri 'self'; connect-src 'self' https://raw.githubusercontent.com; font-src 'self'; frame-src 'self' https://a.sysadmins.zone; img-src 'self' data: http://translate.sysadminsmedia.com; manifest-src 'self'; media-src 'self'; worker-src 'none';
|
||||
7
docs/wrangler.toml
Normal file
7
docs/wrangler.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
name = "homebox-docs"
|
||||
compatibility_date = "2025-07-12"
|
||||
preview_urls = true
|
||||
|
||||
[assets]
|
||||
directory = ".vitepress/dist"
|
||||
not_found_handling = "single-page-application"
|
||||
@@ -2,7 +2,10 @@
|
||||
<Dialog v-if="isDesktop" :dialog-id="dialogId">
|
||||
<DialogScrollContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ title }}</DialogTitle>
|
||||
<div class="mr-4 flex place-items-center justify-between">
|
||||
<DialogTitle>{{ title }}</DialogTitle>
|
||||
<slot name="header-actions" />
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<slot />
|
||||
@@ -29,6 +32,9 @@
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>{{ title }}</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
<div class="flex justify-center">
|
||||
<slot name="header-actions" />
|
||||
</div>
|
||||
|
||||
<div class="m-2 overflow-y-auto p-2">
|
||||
<slot />
|
||||
@@ -39,13 +45,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMediaQuery } from "@vueuse/core";
|
||||
import type { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from "@/components/ui/drawer";
|
||||
import { Dialog, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
|
||||
const isDesktop = useMediaQuery("(min-width: 768px)");
|
||||
|
||||
defineProps<{
|
||||
dialogId: string;
|
||||
dialogId: DialogID;
|
||||
title: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Dialog dialog-id="import">
|
||||
<Dialog :dialog-id="DialogID.Import">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ $t("components.app.import_dialog.title") }}</DialogTitle>
|
||||
@@ -38,6 +38,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import {
|
||||
Dialog,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { DialogID, type NoParamDialogIDs, type OptionalDialogIDs } from "@/components/ui/dialog-provider/utils";
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
@@ -14,7 +15,7 @@
|
||||
|
||||
export type QuickMenuAction =
|
||||
| { text: string; href: string; type: "navigate" }
|
||||
| { text: string; dialogId: string; shortcut: string; type: "create" };
|
||||
| { text: string; dialogId: NoParamDialogIDs | OptionalDialogIDs; shortcut: string; type: "create" };
|
||||
|
||||
const props = defineProps({
|
||||
actions: {
|
||||
@@ -27,11 +28,11 @@
|
||||
const { t } = useI18n();
|
||||
const { closeDialog, openDialog } = useDialog();
|
||||
|
||||
useDialogHotkey("quick-menu", { code: "Backquote", ctrl: true });
|
||||
useDialogHotkey(DialogID.QuickMenu, { code: "Backquote", ctrl: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CommandDialog dialog-id="quick-menu">
|
||||
<CommandDialog :dialog-id="DialogID.QuickMenu">
|
||||
<CommandInput
|
||||
:placeholder="t('components.quick_menu.shortcut_hint')"
|
||||
@keydown="
|
||||
@@ -39,12 +40,12 @@
|
||||
const item = props.actions.filter(item => 'shortcut' in item).find(item => item.shortcut === e.key);
|
||||
if (item) {
|
||||
e.preventDefault();
|
||||
openDialog(item.dialogId);
|
||||
openDialog(item.dialogId as NoParamDialogIDs);
|
||||
}
|
||||
// if esc is pressed, close the dialog
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
closeDialog('quick-menu');
|
||||
closeDialog(DialogID.QuickMenu);
|
||||
}
|
||||
}
|
||||
"
|
||||
@@ -60,7 +61,7 @@
|
||||
@select="
|
||||
e => {
|
||||
e.preventDefault();
|
||||
openDialog(create.dialogId);
|
||||
openDialog(create.dialogId as NoParamDialogIDs);
|
||||
}
|
||||
"
|
||||
>
|
||||
@@ -76,7 +77,7 @@
|
||||
:value="`global.navigate_${i + 1}`"
|
||||
@select="
|
||||
() => {
|
||||
closeDialog('quick-menu');
|
||||
closeDialog(DialogID.QuickMenu);
|
||||
navigateTo(navigate.href);
|
||||
}
|
||||
"
|
||||
@@ -87,8 +88,8 @@
|
||||
value="scanner"
|
||||
@select="
|
||||
() => {
|
||||
closeDialog('quick-menu');
|
||||
openDialog('scanner');
|
||||
closeDialog(DialogID.QuickMenu);
|
||||
openDialog(DialogID.Scanner);
|
||||
}
|
||||
"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Dialog dialog-id="scanner">
|
||||
<Dialog :dialog-id="DialogID.Scanner">
|
||||
<DialogScrollContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("scanner.title") }}</DialogTitle>
|
||||
@@ -13,6 +13,25 @@
|
||||
<MdiAlertCircleOutline class="text-destructive" />
|
||||
<span class="text-sm font-medium">{{ errorMessage }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="detectedBarcode"
|
||||
class="mb-5 flex flex-col items-center gap-2 rounded-md border border-accent-foreground bg-accent p-4 text-accent-foreground"
|
||||
role="alert"
|
||||
>
|
||||
<div class="flex">
|
||||
<MdiBarcode class="mr-2" />
|
||||
<span class="flex-1 text-center text-sm font-medium">
|
||||
{{ detectedBarcodeType }} {{ $t("scanner.barcode_detected_message") }}:
|
||||
<strong>{{ detectedBarcode }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ButtonGroup>
|
||||
<Button :disabled="loading" type="submit" @click="handleButtonClick">
|
||||
{{ $t("scanner.barcode_fetch_data") }}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<!-- eslint-disable-next-line tailwindcss/no-custom-classname -->
|
||||
<video ref="video" class="aspect-video w-full rounded-lg bg-muted shadow" poster="data:image/gif,AAAA"></video>
|
||||
<div class="mt-4">
|
||||
@@ -34,16 +53,19 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { BrowserMultiFormatReader, NotFoundException } from "@zxing/library";
|
||||
import { BrowserMultiFormatReader, NotFoundException, BarcodeFormat } from "@zxing/library";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { Dialog, DialogHeader, DialogTitle, DialogScrollContent } from "@/components/ui/dialog";
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import MdiBarcode from "~icons/mdi/barcode";
|
||||
import MdiAlertCircleOutline from "~icons/mdi/alert-circle-outline";
|
||||
import { useDialog } from "@/components/ui/dialog-provider";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { activeDialog } = useDialog();
|
||||
const open = computed(() => activeDialog.value === "scanner");
|
||||
const { activeDialog, openDialog, closeDialog } = useDialog();
|
||||
const open = computed(() => activeDialog && activeDialog.value === DialogID.Scanner);
|
||||
|
||||
const sources = ref<MediaDeviceInfo[]>([]);
|
||||
const selectedSource = ref<string | null>(null);
|
||||
@@ -51,6 +73,8 @@
|
||||
const video = ref<HTMLVideoElement>();
|
||||
const codeReader = new BrowserMultiFormatReader();
|
||||
const errorMessage = ref<string | null>(null);
|
||||
const detectedBarcode = ref<string>("");
|
||||
const detectedBarcodeType = ref<string>("");
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
console.error("Scanner error:", error);
|
||||
@@ -68,6 +92,10 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleButtonClick = () => {
|
||||
openDialog(DialogID.ProductImport, { params: { barcode: detectedBarcode.value } });
|
||||
};
|
||||
|
||||
const startScanner = async () => {
|
||||
errorMessage.value = null;
|
||||
if (!(navigator && navigator.mediaDevices && "enumerateDevices" in navigator.mediaDevices)) {
|
||||
@@ -109,6 +137,7 @@
|
||||
|
||||
watch(open, async isOpen => {
|
||||
if (isOpen) {
|
||||
detectedBarcode.value = "";
|
||||
await startScanner();
|
||||
} else {
|
||||
stopScanner();
|
||||
@@ -129,10 +158,27 @@
|
||||
throw new Error(t("scanner.invalid_url"));
|
||||
}
|
||||
const sanitizedPath = url.pathname.replace(/[^a-zA-Z0-9-_/]/g, "");
|
||||
closeDialog(DialogID.Scanner);
|
||||
navigateTo(sanitizedPath);
|
||||
} catch (err) {
|
||||
// Check if it's a barcode for a new element
|
||||
const bcfmt = result.getBarcodeFormat();
|
||||
|
||||
switch (bcfmt) {
|
||||
case BarcodeFormat.EAN_13:
|
||||
case BarcodeFormat.UPC_A:
|
||||
case BarcodeFormat.UPC_E:
|
||||
case BarcodeFormat.UPC_EAN_EXTENSION:
|
||||
console.info("Barcode detected");
|
||||
detectedBarcode.value = result.getText();
|
||||
detectedBarcodeType.value = BarcodeFormat[bcfmt].replaceAll("_", "-");
|
||||
break;
|
||||
|
||||
default:
|
||||
handleError(err);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
handleError(err);
|
||||
}
|
||||
}
|
||||
if (err && !(err instanceof NotFoundException)) {
|
||||
@@ -149,9 +195,3 @@
|
||||
stopScanner();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
video {
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
|
||||
45
frontend/components/App/ThemePicker.vue
Normal file
45
frontend/components/App/ThemePicker.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { themes } from "~~/lib/data/themes";
|
||||
import { useTheme } from "~/composables/use-theme";
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="homebox grid grid-cols-1 gap-4 font-sans sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
|
||||
<div
|
||||
v-for="theme in themes"
|
||||
:key="theme.value"
|
||||
:class="'theme-' + theme.value"
|
||||
class="overflow-hidden rounded-lg border outline-2 outline-offset-2"
|
||||
:data-theme="theme.value"
|
||||
:data-set-theme="theme.value"
|
||||
data-act-class="outline"
|
||||
@click="setTheme(theme.value)"
|
||||
>
|
||||
<div :data-theme="theme.value" class="w-full cursor-pointer bg-background-accent text-foreground">
|
||||
<div class="grid grid-cols-5 grid-rows-3">
|
||||
<div class="col-start-1 row-start-1 bg-background"></div>
|
||||
<div class="col-start-1 row-start-2 bg-sidebar"></div>
|
||||
<div class="col-start-1 row-start-3 bg-background-accent"></div>
|
||||
<div class="col-span-4 col-start-2 row-span-3 row-start-1 flex flex-col gap-1 bg-background p-2">
|
||||
<div class="font-bold">{{ theme.label }}</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<div class="flex size-5 items-center justify-center rounded bg-primary lg:size-6">
|
||||
<div class="text-sm font-bold text-primary-foreground">A</div>
|
||||
</div>
|
||||
<div class="flex size-5 items-center justify-center rounded bg-secondary lg:size-6">
|
||||
<div class="text-sm font-bold text-secondary-foreground">A</div>
|
||||
</div>
|
||||
<div class="flex size-5 items-center justify-center rounded bg-accent lg:size-6">
|
||||
<div class="text-sm font-bold text-accent-foreground">A</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -15,6 +15,7 @@
|
||||
import "@vuepic/vue-datepicker/dist/main.css";
|
||||
import * as datelib from "~/lib/datelib/datelib";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { darkThemes } from "~/lib/data/themes";
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "update:text"]);
|
||||
|
||||
@@ -34,7 +35,7 @@
|
||||
},
|
||||
});
|
||||
|
||||
const isDark = useIsDark();
|
||||
const isDark = useIsThemeInList(darkThemes);
|
||||
|
||||
const formatDate = (date: Date | string | number) => fmtDate(date, "human", "date");
|
||||
|
||||
|
||||
256
frontend/components/Item/BarcodeModal.vue
Normal file
256
frontend/components/Item/BarcodeModal.vue
Normal file
@@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<Dialog :dialog-id="DialogID.ProductImport">
|
||||
<DialogContent :class="'w-full md:max-w-xl lg:max-w-4xl'">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ $t("components.item.product_import.title") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
class="flex items-center gap-2 rounded-md border border-destructive bg-destructive/10 p-4 text-destructive"
|
||||
role="alert"
|
||||
>
|
||||
<MdiAlertCircleOutline class="text-destructive" />
|
||||
<span class="text-sm font-medium">{{ errorMessage }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<FormTextField
|
||||
v-model="barcode"
|
||||
:disabled="searching"
|
||||
class="w-[30%]"
|
||||
:label="$t('components.item.product_import.barcode')"
|
||||
@keyup.enter="retrieveProductInfo(barcode)"
|
||||
/>
|
||||
<Button
|
||||
:variant="searching ? 'destructive' : 'default'"
|
||||
class="mt-auto h-10"
|
||||
@click="retrieveProductInfo(barcode)"
|
||||
>
|
||||
<MdiLoading v-if="searching" class="animate-spin" />
|
||||
<div v-if="!searching" class="relative mx-2">
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<MdiBarcode class="size-5 group-hover:hidden" />
|
||||
</div>
|
||||
</div>
|
||||
{{ searching ? $t("global.cancel") : $t("components.item.product_import.search_item") }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<BaseCard>
|
||||
<Table class="w-full">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead
|
||||
v-for="h in headers"
|
||||
:key="h.value"
|
||||
class="text-no-transform bg-secondary text-sm text-secondary-foreground hover:bg-secondary/90"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-1"
|
||||
:class="{
|
||||
'justify-center': h.align === 'center',
|
||||
}"
|
||||
>
|
||||
<template v-if="typeof h === 'string'">{{ h }}</template>
|
||||
<template v-else>{{ $t(h.text) }}</template>
|
||||
</div>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
<TableRow
|
||||
v-for="(p, index) in products"
|
||||
:key="index"
|
||||
class="cursor-pointer"
|
||||
:class="{ selected: selectedRow === index }"
|
||||
@click="selectProduct(index)"
|
||||
>
|
||||
<TableCell
|
||||
v-for="h in headers"
|
||||
:key="h.value"
|
||||
:class="{
|
||||
'text-center': h.align === 'center',
|
||||
}"
|
||||
>
|
||||
<template v-if="h.type === 'name'">
|
||||
<div class="flex items-center space-x-4">
|
||||
<img :src="p.imageBase64" class="w-16 rounded object-fill shadow-sm" alt="Product's photo" />
|
||||
<span class="text-sm font-medium">
|
||||
{{ p.item.name }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="h.type === 'url'">
|
||||
<NuxtLink class="underline" :to="'https://' + extractValue(p, h.value)" target="_blank">{{
|
||||
extractValue(p, h.value)
|
||||
}}</NuxtLink>
|
||||
</template>
|
||||
|
||||
<slot v-else :name="cell(h)">
|
||||
{{ extractValue(p, h.value) }}
|
||||
</slot>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</BaseCard>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="import" :disabled="selectedRow === -1" @click="createItem"> Import selected </Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import type { BarcodeProduct } from "~~/lib/api/types/data-contracts";
|
||||
import { useDialog } from "~/components/ui/dialog-provider";
|
||||
import MdiAlertCircleOutline from "~icons/mdi/alert-circle-outline";
|
||||
import MdiBarcode from "~icons/mdi/barcode";
|
||||
import MdiLoading from "~icons/mdi/loading";
|
||||
import type { TableData } from "~/components/Item/View/Table.types";
|
||||
|
||||
const { openDialog, registerOpenDialogCallback } = useDialog();
|
||||
const { t } = useI18n();
|
||||
|
||||
const searching = ref(false);
|
||||
const barcode = ref<string>("");
|
||||
const products = ref<BarcodeProduct[] | null>(null);
|
||||
const selectedRow = ref(-1);
|
||||
const errorMessage = ref<string | null>(null);
|
||||
|
||||
type BarcodeTableHeader = {
|
||||
text: string;
|
||||
value: string;
|
||||
align?: "left" | "center" | "right";
|
||||
type?: "name" | "url";
|
||||
};
|
||||
|
||||
const defaultHeaders = [
|
||||
{
|
||||
text: "items.name",
|
||||
value: "name",
|
||||
align: "center",
|
||||
type: "name",
|
||||
},
|
||||
{ text: "items.manufacturer", value: "manufacturer", align: "center" },
|
||||
{ text: "items.model_number", value: "modelNumber", align: "center" },
|
||||
{ text: "components.item.product_import.db_source", value: "search_engine_name", align: "center", type: "url" },
|
||||
] satisfies BarcodeTableHeader[];
|
||||
|
||||
// Need for later filtering
|
||||
const headers = defaultHeaders;
|
||||
|
||||
onMounted(() => {
|
||||
const cleanup = registerOpenDialogCallback(DialogID.ProductImport, params => {
|
||||
selectedRow.value = -1;
|
||||
searching.value = false;
|
||||
errorMessage.value = null;
|
||||
|
||||
if (params?.barcode) {
|
||||
// Reset if the barcode is different
|
||||
if (params.barcode !== barcode.value) {
|
||||
barcode.value = params.barcode;
|
||||
|
||||
retrieveProductInfo(barcode.value).then(() => {
|
||||
console.log("Processing finished");
|
||||
});
|
||||
}
|
||||
} else {
|
||||
barcode.value = "";
|
||||
products.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(cleanup);
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
function createItem() {
|
||||
if (
|
||||
products.value !== null &&
|
||||
products.value.length > 0 &&
|
||||
selectedRow.value >= 0 &&
|
||||
selectedRow.value < products.value.length
|
||||
) {
|
||||
const p = products.value![selectedRow.value];
|
||||
openDialog(DialogID.CreateItem, {
|
||||
params: { product: p },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function retrieveProductInfo(barcode: string) {
|
||||
errorMessage.value = null;
|
||||
|
||||
if (!barcode || barcode.trim().length === 0 || !/^[0-9]+$/.test(barcode)) {
|
||||
errorMessage.value = t("components.item.product_import.error_invalid_barcode");
|
||||
console.error(errorMessage.value);
|
||||
return;
|
||||
}
|
||||
|
||||
products.value = null;
|
||||
searching.value = true;
|
||||
|
||||
try {
|
||||
const result = await api.products.searchFromBarcode(barcode.trim());
|
||||
if (result.error) {
|
||||
errorMessage.value = t("errors.api_failure") + result.error;
|
||||
console.error(errorMessage.value);
|
||||
} else {
|
||||
if (result.data === undefined || result.data.length === undefined || result.data.length === 0) {
|
||||
errorMessage.value = t("components.item.product_import.error_not_found");
|
||||
}
|
||||
|
||||
products.value = result.data;
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessage.value = t("components.item.product_import.error_exception") + error;
|
||||
console.error(errorMessage.value);
|
||||
} finally {
|
||||
searching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function extractValue(data: TableData, value: string) {
|
||||
const parts = value.split(".");
|
||||
let current = data;
|
||||
for (const part of parts) {
|
||||
current = current[part];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function cell(h: BarcodeTableHeader) {
|
||||
return `cell-${h.value.replace(".", "_")}`;
|
||||
}
|
||||
|
||||
function selectProduct(index: number) {
|
||||
// Unselect if already selected
|
||||
if (selectedRow.value === index) {
|
||||
selectedRow.value = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
selectedRow.value = index;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
tr.selected {
|
||||
background-color: hsl(var(--primary));
|
||||
color: hsl(var(--background));
|
||||
}
|
||||
|
||||
tr:hover.selected {
|
||||
background-color: hsl(var(--primary));
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,34 @@
|
||||
<template>
|
||||
<BaseModal dialog-id="create-item" :title="$t('components.item.create_modal.title')">
|
||||
<BaseModal :dialog-id="DialogID.CreateItem" :title="$t('components.item.create_modal.title')">
|
||||
<template #header-actions>
|
||||
<div class="flex">
|
||||
<TooltipProvider :delay-duration="0">
|
||||
<ButtonGroup>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button variant="outline" :disabled="loading" size="icon" data-pos="start" @click="openQrScannerPage()">
|
||||
<MdiBarcodeScan class="size-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{{ $t("components.item.create_modal.product_tooltip_scan_barcode") }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button variant="outline" :disabled="loading" size="icon" data-pos="end" @click="openBarcodeDialog()">
|
||||
<MdiBarcode class="size-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{{ $t("components.item.create_modal.product_tooltip_input_barcode") }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form class="flex flex-col gap-2" @submit.prevent="create()">
|
||||
<LocationSelector v-model="form.location" />
|
||||
<ItemSelector
|
||||
@@ -41,7 +70,6 @@
|
||||
class="absolute left-0 top-0 size-full cursor-pointer opacity-0"
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/gif,image/avif,image/webp;capture=camera"
|
||||
capture="environment"
|
||||
multiple
|
||||
@change="previewImage"
|
||||
/>
|
||||
@@ -141,6 +169,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import { Button, ButtonGroup } from "~/components/ui/button";
|
||||
import BaseModal from "@/components/App/CreateModal.vue";
|
||||
@@ -149,6 +178,8 @@
|
||||
import type { ItemCreate, LocationOut } from "~~/lib/api/types/data-contracts";
|
||||
import { useLabelStore } from "~~/stores/labels";
|
||||
import { useLocationStore } from "~~/stores/locations";
|
||||
import MdiBarcode from "~icons/mdi/barcode";
|
||||
import MdiBarcodeScan from "~icons/mdi/barcode-scan";
|
||||
import MdiPackageVariant from "~icons/mdi/package-variant";
|
||||
import MdiPackageVariantClosed from "~icons/mdi/package-variant-closed";
|
||||
import MdiDelete from "~icons/mdi/delete";
|
||||
@@ -168,9 +199,9 @@
|
||||
}
|
||||
|
||||
const { t } = useI18n();
|
||||
const { activeDialog, closeDialog } = useDialog();
|
||||
const { openDialog, closeDialog, registerOpenDialogCallback } = useDialog();
|
||||
|
||||
useDialogHotkey("create-item", { code: "Digit1", shift: true });
|
||||
useDialogHotkey(DialogID.CreateItem, { code: "Digit1", shift: true });
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
@@ -268,55 +299,69 @@
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => activeDialog.value,
|
||||
async active => {
|
||||
if (active === "create-item") {
|
||||
// needed since URL will be cleared in the next step => ParentId Selection should stay though
|
||||
subItemCreate.value = subItemCreateParam.value === "y";
|
||||
let parentItemLocationId = null;
|
||||
onMounted(() => {
|
||||
const cleanup = registerOpenDialogCallback(DialogID.CreateItem, async params => {
|
||||
// needed since URL will be cleared in the next step => ParentId Selection should stay though
|
||||
subItemCreate.value = subItemCreateParam.value === "y";
|
||||
let parentItemLocationId = null;
|
||||
|
||||
if (subItemCreate.value && itemId.value) {
|
||||
const itemIdRead = typeof itemId.value === "string" ? (itemId.value as string) : itemId.value[0];
|
||||
const { data, error } = await api.items.get(itemIdRead);
|
||||
if (error || !data) {
|
||||
toast.error(t("components.item.create_modal.toast.failed_load_parent"));
|
||||
console.error("Parent item fetch error:", error);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
parent.value = data;
|
||||
}
|
||||
|
||||
if (data.location) {
|
||||
const { location } = data;
|
||||
parentItemLocationId = location.id;
|
||||
}
|
||||
|
||||
// clear URL Parameter (subItemCreate) since intention was communicated and received
|
||||
const currentQuery = { ...route.query };
|
||||
delete currentQuery.subItemCreate;
|
||||
await router.push({ query: currentQuery });
|
||||
} else {
|
||||
// since Input is hidden in this case, make sure no accidental parent information is sent out
|
||||
parent.value = {};
|
||||
form.parentId = null;
|
||||
if (subItemCreate.value && itemId.value) {
|
||||
const itemIdRead = typeof itemId.value === "string" ? (itemId.value as string) : itemId.value[0];
|
||||
const { data, error } = await api.items.get(itemIdRead);
|
||||
if (error || !data) {
|
||||
toast.error(t("components.item.create_modal.toast.failed_load_parent"));
|
||||
console.error("Parent item fetch error:", error);
|
||||
}
|
||||
|
||||
const locId = locationId.value ? locationId.value : parentItemLocationId;
|
||||
|
||||
if (locId) {
|
||||
const found = locations.value.find(l => l.id === locId);
|
||||
if (found) {
|
||||
form.location = found;
|
||||
}
|
||||
if (data) {
|
||||
parent.value = data;
|
||||
}
|
||||
if (labelId.value) {
|
||||
form.labels = labels.value.filter(l => l.id === labelId.value).map(l => l.id);
|
||||
|
||||
if (data.location) {
|
||||
const { location } = data;
|
||||
parentItemLocationId = location.id;
|
||||
}
|
||||
|
||||
// clear URL Parameter (subItemCreate) since intention was communicated and received
|
||||
const currentQuery = { ...route.query };
|
||||
delete currentQuery.subItemCreate;
|
||||
await router.push({ query: currentQuery });
|
||||
} else {
|
||||
// since Input is hidden in this case, make sure no accidental parent information is sent out
|
||||
parent.value = {};
|
||||
form.parentId = null;
|
||||
}
|
||||
|
||||
const locId = locationId.value ? locationId.value : parentItemLocationId;
|
||||
|
||||
if (locId) {
|
||||
const found = locations.value.find(l => l.id === locId);
|
||||
if (found) {
|
||||
form.location = found;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (params?.product) {
|
||||
form.name = params.product.item.name;
|
||||
form.description = params.product.item.description;
|
||||
|
||||
if (params.product.imageURL) {
|
||||
form.photos.push({
|
||||
photoName: "product_view.jpg",
|
||||
fileBase64: params.product.imageBase64,
|
||||
primary: form.photos.length === 0,
|
||||
file: dataURLtoFile(params.product.imageBase64, "product_view.jpg"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (labelId.value) {
|
||||
form.labels = labels.value.filter(l => l.id === labelId.value).map(l => l.id);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(cleanup);
|
||||
});
|
||||
|
||||
async function create(close = true) {
|
||||
if (!form.location?.id) {
|
||||
@@ -387,7 +432,7 @@
|
||||
loading.value = false;
|
||||
|
||||
if (close) {
|
||||
closeDialog("create-item");
|
||||
closeDialog(DialogID.CreateItem);
|
||||
navigateTo(`/item/${data.id}`);
|
||||
}
|
||||
}
|
||||
@@ -466,4 +511,12 @@
|
||||
offScreenCanvas.height = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function openQrScannerPage() {
|
||||
openDialog(DialogID.Scanner);
|
||||
}
|
||||
|
||||
function openBarcodeDialog() {
|
||||
openDialog(DialogID.ProductImport);
|
||||
}
|
||||
</script>
|
||||
|
||||
83
frontend/components/Item/DuplicateSettings.vue
Normal file
83
frontend/components/Item/DuplicateSettings.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import type { DuplicateSettings } from "~/composables/use-preferences";
|
||||
|
||||
type Props = {
|
||||
modelValue: DuplicateSettings;
|
||||
};
|
||||
|
||||
type Emits = {
|
||||
(e: "update:modelValue", value: DuplicateSettings): void;
|
||||
};
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const enableCustomPrefix = ref(props.modelValue.copyPrefixOverride !== null);
|
||||
const prefix = ref(props.modelValue.copyPrefixOverride ?? "");
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const settings = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit("update:modelValue", value),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch id="copy-maintenance" v-model="settings.copyMaintenance" />
|
||||
<Label for="copy-maintenance">
|
||||
{{ $t("items.duplicate.copy_maintenance") }}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch id="copy-attachments" v-model="settings.copyAttachments" />
|
||||
<Label for="copy-attachments">
|
||||
{{ $t("items.duplicate.copy_attachments") }}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch id="copy-custom-fields" v-model="settings.copyCustomFields" />
|
||||
<Label for="copy-custom-fields">
|
||||
{{ $t("items.duplicate.copy_custom_fields") }}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch
|
||||
id="copy-prefix"
|
||||
v-model="enableCustomPrefix"
|
||||
@update:model-value="
|
||||
v => {
|
||||
settings.copyPrefixOverride = v ? prefix : null;
|
||||
}
|
||||
"
|
||||
/>
|
||||
<Label for="copy-prefix">{{ $t("items.duplicate.enable_custom_prefix") }}</Label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label for="copy-prefix" :class="{ 'opacity-50': !enableCustomPrefix }">
|
||||
{{ $t("items.duplicate.custom_prefix") }}
|
||||
</Label>
|
||||
<Input
|
||||
id="copy-prefix"
|
||||
v-model="prefix"
|
||||
:disabled="!enableCustomPrefix"
|
||||
:placeholder="$t('items.duplicate.prefix')"
|
||||
class="w-full"
|
||||
@input="settings.copyPrefixOverride = prefix"
|
||||
/>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ $t("items.duplicate.prefix_instructions") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
99
frontend/components/Item/ImageDialog.vue
Normal file
99
frontend/components/Item/ImageDialog.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
import { buttonVariants, Button } from "@/components/ui/button";
|
||||
import { useDialog } from "@/components/ui/dialog-provider";
|
||||
import { DialogID } from "~/components/ui/dialog-provider/utils";
|
||||
import { useConfirm } from "@/composables/use-confirm";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import MdiClose from "~icons/mdi/close";
|
||||
import MdiDownload from "~icons/mdi/download";
|
||||
import MdiDelete from "~icons/mdi/delete";
|
||||
|
||||
const { t } = useI18n();
|
||||
const confirm = useConfirm();
|
||||
|
||||
const { closeDialog, registerOpenDialogCallback } = useDialog();
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
const image = reactive<{
|
||||
attachmentId: string;
|
||||
itemId: string;
|
||||
originalSrc: string;
|
||||
originalType?: string;
|
||||
thumbnailSrc?: string;
|
||||
}>({
|
||||
attachmentId: "",
|
||||
itemId: "",
|
||||
originalSrc: "",
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const cleanup = registerOpenDialogCallback(DialogID.ItemImage, params => {
|
||||
image.attachmentId = params.attachmentId;
|
||||
image.itemId = params.itemId;
|
||||
if (params.type === "preloaded") {
|
||||
image.originalSrc = params.originalSrc;
|
||||
image.originalType = params.originalType;
|
||||
image.thumbnailSrc = params.thumbnailSrc;
|
||||
} else if (params.type === "attachment") {
|
||||
image.originalSrc = api.authURL(`/items/${params.itemId}/attachments/${params.attachmentId}`);
|
||||
image.originalType = params.mimeType;
|
||||
image.thumbnailSrc = params.thumbnailId
|
||||
? api.authURL(`/items/${params.itemId}/attachments/${params.thumbnailId}`)
|
||||
: image.originalSrc;
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(cleanup);
|
||||
});
|
||||
|
||||
async function deleteAttachment() {
|
||||
const confirmed = await confirm.open(t("items.delete_attachment_confirm"));
|
||||
|
||||
if (confirmed.isCanceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await api.items.attachments.delete(image.itemId, image.attachmentId);
|
||||
|
||||
if (error) {
|
||||
toast.error(t("items.toast.failed_delete_attachment"));
|
||||
return;
|
||||
}
|
||||
|
||||
closeDialog(DialogID.ItemImage, {
|
||||
action: "delete",
|
||||
id: image.attachmentId,
|
||||
});
|
||||
toast.success(t("items.toast.attachment_deleted"));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :dialog-id="DialogID.ItemImage">
|
||||
<DialogContent class="w-auto border-transparent bg-transparent p-0" disable-close>
|
||||
<picture>
|
||||
<source :srcset="image.originalSrc" :type="image.originalType" />
|
||||
<img :src="image.thumbnailSrc" alt="attachment image" />
|
||||
</picture>
|
||||
<Button variant="destructive" size="icon" class="absolute right-[84px] top-1" @click="deleteAttachment">
|
||||
<MdiDelete />
|
||||
</Button>
|
||||
<a :class="buttonVariants({ size: 'icon' })" :href="image.originalSrc" download class="absolute right-11 top-1">
|
||||
<MdiDownload />
|
||||
</a>
|
||||
<Button
|
||||
size="icon"
|
||||
class="absolute right-1 top-1"
|
||||
@click="
|
||||
closeDialog(DialogID.ItemImage);
|
||||
image.originalSrc = '';
|
||||
"
|
||||
>
|
||||
<MdiClose />
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Dialog dialog-id="item-table-settings">
|
||||
<Dialog :dialog-id="DialogID.ItemTableSettings">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ $t("components.item.view.table.table_settings") }}</DialogTitle>
|
||||
@@ -41,7 +41,7 @@
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button @click="closeDialog('item-table-settings')"> {{ $t("global.save") }} </Button>
|
||||
<Button @click="closeDialog(DialogID.ItemTableSettings)"> {{ $t("global.save") }} </Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -123,7 +123,7 @@
|
||||
hidden: disableControls,
|
||||
}"
|
||||
>
|
||||
<Button class="size-10 p-0" variant="outline" @click="openDialog('item-table-settings')">
|
||||
<Button class="size-10 p-0" variant="outline" @click="openDialog(DialogID.ItemTableSettings)">
|
||||
<MdiTableCog />
|
||||
</Button>
|
||||
<Pagination
|
||||
@@ -174,6 +174,7 @@
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { useDialog } from "@/components/ui/dialog-provider";
|
||||
import { DialogID } from "~/components/ui/dialog-provider/utils";
|
||||
|
||||
const { openDialog, closeDialog } = useDialog();
|
||||
|
||||
|
||||
@@ -42,6 +42,6 @@
|
||||
<MdiArrowUp class="hidden group-hover/label-chip:block" />
|
||||
</div>
|
||||
</div>
|
||||
{{ label.name.length > 20 ? `${label.name.substring(0, 20)}...` : label.name }}
|
||||
{{ label.name }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<BaseModal dialog-id="create-label" :title="$t('components.label.create_modal.title')">
|
||||
<BaseModal :dialog-id="DialogID.CreateLabel" :title="$t('components.label.create_modal.title')">
|
||||
<form class="flex flex-col gap-2" @submit.prevent="create()">
|
||||
<FormTextField
|
||||
v-model="form.name"
|
||||
@@ -12,7 +12,7 @@
|
||||
<FormTextArea
|
||||
v-model="form.description"
|
||||
:label="$t('components.label.create_modal.label_description')"
|
||||
:max-length="255"
|
||||
:max-length="1000"
|
||||
/>
|
||||
<ColorSelector v-model="form.color" :label="$t('components.label.create_modal.label_color')" :show-hex="true" />
|
||||
<div class="mt-4 flex flex-row-reverse">
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import BaseModal from "@/components/App/CreateModal.vue";
|
||||
import { useDialog, useDialogHotkey } from "~/components/ui/dialog-provider";
|
||||
@@ -38,7 +39,7 @@
|
||||
|
||||
const { closeDialog } = useDialog();
|
||||
|
||||
useDialogHotkey("create-label", { code: "Digit2", shift: true });
|
||||
useDialogHotkey(DialogID.CreateLabel, { code: "Digit2", shift: true });
|
||||
|
||||
const loading = ref(false);
|
||||
const focused = ref(false);
|
||||
@@ -85,7 +86,7 @@
|
||||
reset();
|
||||
|
||||
if (close) {
|
||||
closeDialog("create-label");
|
||||
closeDialog(DialogID.CreateLabel);
|
||||
navigateTo(`/label/${data.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,16 @@
|
||||
<TagsInput
|
||||
v-model="modelValue"
|
||||
class="w-full gap-0 px-0"
|
||||
:display-value="v => shortenedLabels.find(l => l.id === v)?.name ?? 'Loading...'"
|
||||
:display-value="v => props.labels.find(l => l.id === v)?.name ?? 'Loading...'"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2 px-3">
|
||||
<TagsInputItem v-for="item in modelValue" :key="item" :value="item">
|
||||
<div class="flex flex-wrap items-center gap-2 overflow-hidden px-3">
|
||||
<TagsInputItem v-for="item in modelValue" :key="item" :value="item" class="h-auto overflow-hidden text-wrap">
|
||||
<span
|
||||
v-if="shortenedLabels.find(l => l.id === item)?.color"
|
||||
class="ml-2 inline-block size-4 rounded-full"
|
||||
:style="{ backgroundColor: shortenedLabels.find(l => l.id === item)?.color }"
|
||||
v-if="props.labels.find(l => l.id === item)?.color"
|
||||
class="ml-2 size-4 shrink-0 rounded-full"
|
||||
:style="{ backgroundColor: props.labels.find(l => l.id === item)?.color }"
|
||||
/>
|
||||
<TagsInputItemText />
|
||||
<TagsInputItemText class="py-0.5" />
|
||||
<TagsInputItemDelete />
|
||||
</TagsInputItem>
|
||||
</div>
|
||||
@@ -61,9 +61,9 @@
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="mr-2 inline-block size-4 rounded-full align-middle"
|
||||
:class="{ border: shortenedLabels.find(l => l.id === label.value)?.color }"
|
||||
:style="{ backgroundColor: shortenedLabels.find(l => l.id === label.value)?.color }"
|
||||
class="mr-2 size-4 shrink-0 rounded-full align-middle"
|
||||
:class="{ border: props.labels.find(l => l.id === label.value)?.color }"
|
||||
:style="{ backgroundColor: props.labels.find(l => l.id === label.value)?.color }"
|
||||
/>
|
||||
{{ label.label }}
|
||||
</CommandItem>
|
||||
@@ -114,24 +114,23 @@
|
||||
const open = ref(false);
|
||||
const searchTerm = ref("");
|
||||
|
||||
const shortenedLabels = computed(() => {
|
||||
return props.labels.map(l => ({
|
||||
...l,
|
||||
name: l.name.length > 20 ? `${l.name.substring(0, 20)}...` : l.name,
|
||||
}));
|
||||
});
|
||||
|
||||
const filteredLabels = computed(() => {
|
||||
const filtered = fuzzysort
|
||||
.go(searchTerm.value, shortenedLabels.value, { key: "name", all: true })
|
||||
.go(searchTerm.value, props.labels, { key: "name", all: true })
|
||||
.map(l => ({
|
||||
value: l.obj.id,
|
||||
label: l.obj.name,
|
||||
}))
|
||||
.filter(i => !modelValue.value.includes(i.value));
|
||||
|
||||
// Only show "Create" option if search term is not empty and no exact match exists
|
||||
if (searchTerm.value.trim() !== "") {
|
||||
filtered.push({ value: "create-item", label: `${t("global.create")} ${searchTerm.value}` });
|
||||
const trimmedSearchTerm = searchTerm.value.trim();
|
||||
const hasExactMatch = props.labels.some(label => label.name.toLowerCase() === trimmedSearchTerm.toLowerCase());
|
||||
|
||||
if (!hasExactMatch) {
|
||||
filtered.push({ value: "create-item", label: `${t("global.create")} ${searchTerm.value}` });
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<BaseModal dialog-id="create-location" :title="$t('components.location.create_modal.title')">
|
||||
<BaseModal :dialog-id="DialogID.CreateLocation" :title="$t('components.location.create_modal.title')">
|
||||
<form class="flex flex-col gap-2" @submit.prevent="create()">
|
||||
<LocationSelector v-model="form.parent" />
|
||||
<FormTextField
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import { Button, ButtonGroup } from "~/components/ui/button";
|
||||
import BaseModal from "@/components/App/CreateModal.vue";
|
||||
@@ -41,7 +42,7 @@
|
||||
|
||||
const { activeDialog, closeDialog } = useDialog();
|
||||
|
||||
useDialogHotkey("create-location", { code: "Digit3", shift: true });
|
||||
useDialogHotkey(DialogID.CreateLocation, { code: "Digit3", shift: true });
|
||||
|
||||
const loading = ref(false);
|
||||
const focused = ref(false);
|
||||
@@ -54,19 +55,11 @@
|
||||
watch(
|
||||
() => activeDialog.value,
|
||||
active => {
|
||||
if (active === "create-location") {
|
||||
// useTimeoutFn(() => {
|
||||
// focused.value = true;
|
||||
// }, 50);
|
||||
|
||||
if (active && active === DialogID.CreateLocation) {
|
||||
if (locationId.value) {
|
||||
const found = locations.value.find(l => l.id === locationId.value);
|
||||
if (found) {
|
||||
form.parent = found;
|
||||
}
|
||||
form.parent = found || null;
|
||||
}
|
||||
} else {
|
||||
// focused.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -74,7 +67,6 @@
|
||||
function reset() {
|
||||
form.name = "";
|
||||
form.description = "";
|
||||
form.parent = null;
|
||||
focused.value = false;
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -118,10 +110,11 @@
|
||||
if (data) {
|
||||
toast.success(t("components.location.create_modal.toast.create_success"));
|
||||
}
|
||||
|
||||
reset();
|
||||
|
||||
if (close) {
|
||||
closeDialog("create-location");
|
||||
closeDialog(DialogID.CreateLocation);
|
||||
navigateTo(`/location/${data.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Dialog dialog-id="edit-maintenance">
|
||||
<Dialog :dialog-id="DialogID.EditMaintenance">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import type { MaintenanceEntry, MaintenanceEntryWithDetails } from "~~/lib/api/types/data-contracts";
|
||||
import MdiPost from "~icons/mdi/post";
|
||||
@@ -77,7 +78,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
closeDialog("edit-maintenance");
|
||||
closeDialog(DialogID.EditMaintenance);
|
||||
emit("changed");
|
||||
}
|
||||
|
||||
@@ -99,7 +100,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
closeDialog("edit-maintenance");
|
||||
closeDialog(DialogID.EditMaintenance);
|
||||
emit("changed");
|
||||
}
|
||||
|
||||
@@ -111,7 +112,7 @@
|
||||
entry.description = "";
|
||||
entry.cost = "";
|
||||
entry.itemId = itemId;
|
||||
openDialog("edit-maintenance");
|
||||
openDialog(DialogID.EditMaintenance);
|
||||
};
|
||||
|
||||
const openUpdateModal = (maintenanceEntry: MaintenanceEntry | MaintenanceEntryWithDetails) => {
|
||||
@@ -122,7 +123,7 @@
|
||||
entry.description = maintenanceEntry.description;
|
||||
entry.cost = maintenanceEntry.cost;
|
||||
entry.itemId = null;
|
||||
openDialog("edit-maintenance");
|
||||
openDialog(DialogID.EditMaintenance);
|
||||
};
|
||||
|
||||
const confirm = useConfirm();
|
||||
@@ -164,7 +165,7 @@
|
||||
entry.description = maintenanceEntry.description;
|
||||
entry.cost = maintenanceEntry.cost;
|
||||
entry.itemId = itemId;
|
||||
openDialog("edit-maintenance");
|
||||
openDialog(DialogID.EditMaintenance);
|
||||
}
|
||||
|
||||
defineExpose({ openCreateModal, openUpdateModal, deleteEntry, complete, duplicate });
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { route } from "../../lib/api/base";
|
||||
import PageQRCode from "./PageQRCode.vue";
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import MdiLoading from "~icons/mdi/loading";
|
||||
import MdiPrinterPos from "~icons/mdi/printer-pos";
|
||||
@@ -63,7 +64,7 @@
|
||||
}
|
||||
|
||||
toast.success(t("components.global.label_maker.toast.print_success"));
|
||||
closeDialog("print-label");
|
||||
closeDialog(DialogID.PrintLabel);
|
||||
serverPrinting.value = false;
|
||||
}
|
||||
|
||||
@@ -93,7 +94,7 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Dialog dialog-id="print-label">
|
||||
<Dialog :dialog-id="DialogID.PrintLabel">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
@@ -137,7 +138,7 @@
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button size="icon" @click="openDialog('print-label')">
|
||||
<Button size="icon" @click="openDialog(DialogID.PrintLabel)">
|
||||
<MdiPrinterPos name="mdi-printer-pos" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import MarkdownIt from "markdown-it";
|
||||
import { imgSize } from "@mdit/plugin-img-size";
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
type Props = {
|
||||
@@ -14,7 +15,7 @@
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
});
|
||||
}).use(imgSize);
|
||||
|
||||
const raw = computed(() => {
|
||||
const html = md.render(props.source || "").replace(/\n$/, ""); // remove trailing newline
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { route } from "~/lib/api/base";
|
||||
import MdiQrcode from "~icons/mdi/qrcode";
|
||||
@@ -16,7 +17,7 @@
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog dialog-id="page-qr-code">
|
||||
<Dialog :dialog-id="DialogID.PageQRCode">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
@@ -29,7 +30,7 @@
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button size="icon" @click="openDialog('page-qr-code')">
|
||||
<Button size="icon" @click="openDialog(DialogID.PageQRCode)">
|
||||
<MdiQrcode name="mdi-qrcode" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -25,13 +25,13 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
<template>
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay
|
||||
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||
class="fixed inset-0 z-[60] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||
/>
|
||||
<AlertDialogContent
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
'fixed left-1/2 top-1/2 z-[60] grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
||||
@@ -3,8 +3,9 @@ import type { DialogRootEmits, DialogRootProps } from 'reka-ui'
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||
import { useForwardPropsEmits } from 'reka-ui'
|
||||
import Command from './Command.vue'
|
||||
import type { DialogID } from '@/components/ui/dialog-provider/utils';
|
||||
|
||||
const props = defineProps<DialogRootProps & { dialogId: string }>();
|
||||
const props = defineProps<DialogRootProps & { dialogId: DialogID }>();
|
||||
const emits = defineEmits<DialogRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
|
||||
@@ -1,40 +1,73 @@
|
||||
<!-- DialogProvider.vue -->
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import { provideDialogContext } from "./utils";
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import {
|
||||
provideDialogContext,
|
||||
type DialogID,
|
||||
type DialogParamsMap,
|
||||
} from './utils';
|
||||
|
||||
const activeDialog = ref<string | null>(null);
|
||||
const activeDialog = ref<DialogID | null>(null);
|
||||
const activeAlerts = reactive<string[]>([]);
|
||||
const openDialogCallbacks = new Map<DialogID, (params: any) => void>();
|
||||
|
||||
const openDialog = (dialogId: string) => {
|
||||
if (activeAlerts.length > 0) return;
|
||||
activeDialog.value = dialogId;
|
||||
// onClose for the currently-open dialog (only one dialog can be active)
|
||||
let activeOnCloseCallback: ((result?: any) => void) | undefined;
|
||||
|
||||
const registerOpenDialogCallback = <T extends DialogID>(
|
||||
dialogId: T,
|
||||
callback: (params?: T extends keyof DialogParamsMap ? DialogParamsMap[T] : undefined) => void
|
||||
) => {
|
||||
openDialogCallbacks.set(dialogId, callback as (params: any) => void);
|
||||
return () => {
|
||||
openDialogCallbacks.delete(dialogId);
|
||||
};
|
||||
};
|
||||
|
||||
const closeDialog = (dialogId?: string) => {
|
||||
if (dialogId) {
|
||||
if (activeDialog.value === dialogId) {
|
||||
activeDialog.value = null;
|
||||
}
|
||||
} else {
|
||||
activeDialog.value = null;
|
||||
const openDialog = <T extends DialogID>(dialogId: T, options?: any) => {
|
||||
if (activeAlerts.length > 0) return;
|
||||
|
||||
activeDialog.value = dialogId;
|
||||
activeOnCloseCallback = options?.onClose;
|
||||
|
||||
const openCallback = openDialogCallbacks.get(dialogId);
|
||||
if (openCallback) {
|
||||
openCallback(options?.params);
|
||||
}
|
||||
};
|
||||
|
||||
function closeDialog(dialogId?: DialogID, result?: any) {
|
||||
// No dialogId passed -> close current active dialog without result
|
||||
if (!dialogId) {
|
||||
if (activeDialog.value) {
|
||||
// call onClose (if any) with no result
|
||||
activeOnCloseCallback?.(undefined);
|
||||
activeOnCloseCallback = undefined;
|
||||
}
|
||||
activeDialog.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// dialogId passed -> if it's the active dialog, call onClose with result
|
||||
if (activeDialog.value && activeDialog.value === dialogId) {
|
||||
activeOnCloseCallback?.(result);
|
||||
activeOnCloseCallback = undefined;
|
||||
activeDialog.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
const addAlert = (alertId: string) => {
|
||||
activeAlerts.push(alertId);
|
||||
};
|
||||
|
||||
const removeAlert = (alertId: string) => {
|
||||
const index = activeAlerts.indexOf(alertId);
|
||||
if (index !== -1) {
|
||||
activeAlerts.splice(index, 1);
|
||||
}
|
||||
if (index !== -1) activeAlerts.splice(index, 1);
|
||||
};
|
||||
|
||||
// Provide context to child components
|
||||
provideDialogContext({
|
||||
activeDialog: computed(() => activeDialog.value),
|
||||
registerOpenDialogCallback,
|
||||
openDialog,
|
||||
closeDialog,
|
||||
activeAlerts: computed(() => activeAlerts),
|
||||
|
||||
@@ -1,56 +1,188 @@
|
||||
import type { ComputedRef } from "vue";
|
||||
import { createContext } from "reka-ui";
|
||||
import { useMagicKeys, useActiveElement } from "@vueuse/core";
|
||||
import { computed, type ComputedRef } from 'vue';
|
||||
import { createContext } from 'reka-ui';
|
||||
import { useMagicKeys, useActiveElement } from '@vueuse/core';
|
||||
import type { BarcodeProduct } from '~~/lib/api/types/data-contracts';
|
||||
|
||||
export enum DialogID {
|
||||
AttachmentEdit = 'attachment-edit',
|
||||
ChangePassword = 'changePassword',
|
||||
CreateItem = 'create-item',
|
||||
CreateLocation = 'create-location',
|
||||
CreateLabel = 'create-label',
|
||||
CreateNotifier = 'create-notifier',
|
||||
DuplicateSettings = 'duplicate-settings',
|
||||
DuplicateTemporarySettings = 'duplicate-temporary-settings',
|
||||
EditMaintenance = 'edit-maintenance',
|
||||
Import = 'import',
|
||||
ItemImage = 'item-image',
|
||||
ItemTableSettings = 'item-table-settings',
|
||||
PrintLabel = 'print-label',
|
||||
ProductImport = 'product-import',
|
||||
QuickMenu = 'quick-menu',
|
||||
Scanner = 'scanner',
|
||||
PageQRCode = 'page-qr-code',
|
||||
UpdateLabel = 'update-label',
|
||||
UpdateLocation = 'update-location',
|
||||
}
|
||||
|
||||
/**
|
||||
* - Keys present without ? => params required
|
||||
* - Keys present with ? => params optional
|
||||
* - Keys not present => no params allowed
|
||||
*/
|
||||
export type DialogParamsMap = {
|
||||
[DialogID.ItemImage]:
|
||||
| ({
|
||||
type: 'preloaded';
|
||||
originalSrc: string;
|
||||
originalType?: string;
|
||||
thumbnailSrc?: string;
|
||||
}
|
||||
| {
|
||||
type: 'attachment';
|
||||
mimeType: string;
|
||||
thumbnailId?: string;
|
||||
}) & {
|
||||
itemId: string;
|
||||
attachmentId: string;
|
||||
};
|
||||
[DialogID.CreateItem]?: { product?: BarcodeProduct };
|
||||
[DialogID.ProductImport]?: { barcode?: string };
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines the payload type for a dialog's onClose callback.
|
||||
*/
|
||||
export type DialogResultMap = {
|
||||
[DialogID.ItemImage]?: { action: 'delete', id: string };
|
||||
};
|
||||
|
||||
/** Helpers to split IDs by requirement */
|
||||
type OptionalKeys<T> = {
|
||||
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
type RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;
|
||||
|
||||
type SpecifiedDialogIDs = keyof DialogParamsMap;
|
||||
export type NoParamDialogIDs = Exclude<DialogID, SpecifiedDialogIDs>;
|
||||
export type RequiredDialogIDs = RequiredKeys<DialogParamsMap>;
|
||||
export type OptionalDialogIDs = OptionalKeys<DialogParamsMap>;
|
||||
|
||||
type ParamsOf<T extends DialogID> = T extends SpecifiedDialogIDs
|
||||
? DialogParamsMap[T]
|
||||
: never;
|
||||
|
||||
type ResultOf<T extends DialogID> = T extends keyof DialogResultMap
|
||||
? DialogResultMap[T]
|
||||
: void;
|
||||
|
||||
type OpenDialog = {
|
||||
// Dialogs with no parameters
|
||||
<T extends NoParamDialogIDs>(
|
||||
dialogId: T,
|
||||
options?: { onClose?: (result?: ResultOf<T>) => void; params?: never }
|
||||
): void;
|
||||
// Dialogs with required parameters
|
||||
<T extends RequiredDialogIDs>(
|
||||
dialogId: T,
|
||||
options: { params: ParamsOf<T>; onClose?: (result?: ResultOf<T>) => void }
|
||||
): void;
|
||||
// Dialogs with optional parameters
|
||||
<T extends OptionalDialogIDs>(
|
||||
dialogId: T,
|
||||
options?: { params?: ParamsOf<T>; onClose?: (result?: ResultOf<T>) => void }
|
||||
): void;
|
||||
};
|
||||
|
||||
type CloseDialog = {
|
||||
// Close the currently active dialog, no ID specified. No result payload.
|
||||
(): void;
|
||||
// Close a specific dialog that has a defined result type.
|
||||
<T extends keyof DialogResultMap>(dialogId: T, result?: ResultOf<T>): void;
|
||||
// Close a specific dialog that has NO defined result type.
|
||||
<T extends Exclude<DialogID, keyof DialogResultMap>>(
|
||||
dialogId: T,
|
||||
result?: never
|
||||
): void;
|
||||
};
|
||||
|
||||
type OpenCallback = {
|
||||
<T extends NoParamDialogIDs>(dialogId: T, cb: () => void): () => void;
|
||||
<T extends RequiredDialogIDs>(
|
||||
dialogId: T,
|
||||
cb: (params: ParamsOf<T>) => void
|
||||
): () => void;
|
||||
<T extends OptionalDialogIDs>(
|
||||
dialogId: T,
|
||||
cb: (params?: ParamsOf<T>) => void
|
||||
): () => void;
|
||||
};
|
||||
|
||||
export const [useDialog, provideDialogContext] = createContext<{
|
||||
activeDialog: ComputedRef<string | null>;
|
||||
activeDialog: ComputedRef<DialogID | null>;
|
||||
activeAlerts: ComputedRef<string[]>;
|
||||
openDialog: (dialogId: string) => void;
|
||||
closeDialog: (dialogId?: string) => void;
|
||||
registerOpenDialogCallback: OpenCallback;
|
||||
openDialog: OpenDialog;
|
||||
closeDialog: CloseDialog;
|
||||
addAlert: (alertId: string) => void;
|
||||
removeAlert: (alertId: string) => void;
|
||||
}>("DialogProvider");
|
||||
}>('DialogProvider');
|
||||
|
||||
export const useDialogHotkey = (
|
||||
dialogId: string,
|
||||
key: {
|
||||
shift?: boolean;
|
||||
ctrl?: boolean;
|
||||
code: string;
|
||||
}
|
||||
) => {
|
||||
/**
|
||||
* Hotkey helper:
|
||||
* - No/optional params: pass dialogId + key
|
||||
* - Required params: pass dialogId + key + getParams()
|
||||
*/
|
||||
type HotkeyKey = {
|
||||
shift?: boolean;
|
||||
ctrl?: boolean;
|
||||
code: string;
|
||||
};
|
||||
|
||||
export function useDialogHotkey<T extends NoParamDialogIDs | OptionalDialogIDs>(
|
||||
dialogId: T,
|
||||
key: HotkeyKey
|
||||
): void;
|
||||
export function useDialogHotkey<T extends RequiredDialogIDs>(
|
||||
dialogId: T,
|
||||
key: HotkeyKey,
|
||||
getParams: () => ParamsOf<T>
|
||||
): void;
|
||||
export function useDialogHotkey(
|
||||
dialogId: DialogID,
|
||||
key: HotkeyKey,
|
||||
getParams?: () => unknown
|
||||
) {
|
||||
const { openDialog } = useDialog();
|
||||
|
||||
const activeElement = useActiveElement();
|
||||
|
||||
const notUsingInput = computed(
|
||||
() => activeElement.value?.tagName !== "INPUT" && activeElement.value?.tagName !== "TEXTAREA"
|
||||
() =>
|
||||
activeElement.value?.tagName !== 'INPUT' &&
|
||||
activeElement.value?.tagName !== 'TEXTAREA'
|
||||
);
|
||||
|
||||
useMagicKeys({
|
||||
passive: false,
|
||||
onEventFired: event => {
|
||||
// console.log({
|
||||
// event,
|
||||
// notUsingInput: notUsingInput.value,
|
||||
// eventType: event.type,
|
||||
// keyCode: event.code,
|
||||
// matchingKeyCode: key.code === event.code,
|
||||
// shift: event.shiftKey,
|
||||
// matchingShift: key.shift === undefined || event.shiftKey === key.shift,
|
||||
// ctrl: event.ctrlKey,
|
||||
// matchingCtrl: key.ctrl === undefined || event.ctrlKey === key.ctrl,
|
||||
// });
|
||||
onEventFired: (event) => {
|
||||
if (
|
||||
notUsingInput.value &&
|
||||
event.type === "keydown" &&
|
||||
event.type === 'keydown' &&
|
||||
event.code === key.code &&
|
||||
(key.shift === undefined || event.shiftKey === key.shift) &&
|
||||
(key.ctrl === undefined || event.ctrlKey === key.ctrl)
|
||||
) {
|
||||
openDialog(dialogId);
|
||||
if (getParams) {
|
||||
openDialog(dialogId as RequiredDialogIDs, {
|
||||
params: getParams() as never,
|
||||
});
|
||||
} else {
|
||||
openDialog(dialogId as NoParamDialogIDs);
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogRoot, type DialogRootEmits, type DialogRootProps, useForwardPropsEmits } from "reka-ui";
|
||||
import { useDialog } from "../dialog-provider/utils";
|
||||
import { useDialog, type DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
|
||||
const props = defineProps<DialogRootProps & { dialogId: string }>();
|
||||
const props = defineProps<DialogRootProps & { dialogId: DialogID }>();
|
||||
const emits = defineEmits<DialogRootEmits>();
|
||||
|
||||
const { closeDialog, activeDialog } = useDialog();
|
||||
|
||||
const isOpen = computed(() => activeDialog.value === props.dialogId);
|
||||
const isOpen = computed(() => (activeDialog.value && activeDialog.value === props.dialogId));
|
||||
const onOpenChange = (open: boolean) => {
|
||||
if (!open) closeDialog(props.dialogId);
|
||||
if (!open) closeDialog(props.dialogId as any);
|
||||
};
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
import type { DrawerRootEmits, DrawerRootProps } from "vaul-vue";
|
||||
import { useForwardPropsEmits } from "reka-ui";
|
||||
import { DrawerRoot } from "vaul-vue";
|
||||
import { useDialog } from "../dialog-provider/utils";
|
||||
import { DialogID, useDialog } from "@/components/ui/dialog-provider/utils";
|
||||
|
||||
const props = withDefaults(defineProps<DrawerRootProps & { dialogId: string }>(), {
|
||||
shouldScaleBackground: true,
|
||||
}) as DrawerRootProps & { dialogId: string };
|
||||
}) as DrawerRootProps & { dialogId: DialogID };
|
||||
|
||||
const emits = defineEmits<DrawerRootEmits>();
|
||||
|
||||
const { closeDialog, activeDialog } = useDialog();
|
||||
|
||||
const isOpen = computed(() => activeDialog.value === props.dialogId);
|
||||
const isOpen = computed(() => activeDialog.value !== null && activeDialog.value === props.dialogId);
|
||||
const onOpenChange = (open: boolean) => {
|
||||
if (!open) closeDialog(props.dialogId);
|
||||
if (!open) closeDialog(props.dialogId as any);
|
||||
};
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
|
||||
@@ -4,6 +4,13 @@ import type { DaisyTheme } from "~~/lib/data/themes";
|
||||
|
||||
export type ViewType = "table" | "card" | "tree";
|
||||
|
||||
export type DuplicateSettings = {
|
||||
copyMaintenance: boolean;
|
||||
copyAttachments: boolean;
|
||||
copyCustomFields: boolean;
|
||||
copyPrefixOverride: string | null;
|
||||
};
|
||||
|
||||
export type LocationViewPreferences = {
|
||||
showDetails: boolean;
|
||||
showEmpty: boolean;
|
||||
@@ -15,6 +22,7 @@ export type LocationViewPreferences = {
|
||||
displayLegacyHeader: boolean;
|
||||
language?: string;
|
||||
overrideFormatLocale?: string;
|
||||
duplicateSettings: DuplicateSettings;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -34,6 +42,12 @@ export function useViewPreferences(): Ref<LocationViewPreferences> {
|
||||
displayLegacyHeader: false,
|
||||
language: null,
|
||||
overrideFormatLocale: null,
|
||||
duplicateSettings: {
|
||||
copyMaintenance: false,
|
||||
copyAttachments: true,
|
||||
copyCustomFields: true,
|
||||
copyPrefixOverride: null,
|
||||
},
|
||||
},
|
||||
{ mergeDefaults: true }
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ComputedRef } from "vue";
|
||||
import type { DaisyTheme } from "~~/lib/data/themes";
|
||||
import { type DaisyTheme } from "~~/lib/data/themes";
|
||||
|
||||
export interface UseTheme {
|
||||
theme: ComputedRef<DaisyTheme>;
|
||||
@@ -42,27 +42,11 @@ export function useTheme(): UseTheme {
|
||||
return { theme, setTheme };
|
||||
}
|
||||
|
||||
export function useIsDark() {
|
||||
export function useIsThemeInList(list: DaisyTheme[]) {
|
||||
const theme = useTheme();
|
||||
|
||||
const darkthemes = [
|
||||
"synthwave",
|
||||
"retro",
|
||||
"cyberpunk",
|
||||
"valentine",
|
||||
"halloween",
|
||||
"forest",
|
||||
"aqua",
|
||||
"black",
|
||||
"luxury",
|
||||
"dracula",
|
||||
"business",
|
||||
"night",
|
||||
"coffee",
|
||||
];
|
||||
|
||||
return computed(() => {
|
||||
return darkthemes.includes(theme.theme.value);
|
||||
return list.includes(theme.theme.value);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<ItemCreateModal />
|
||||
<LabelCreateModal />
|
||||
<LocationCreateModal />
|
||||
<ItemBarcodeModal />
|
||||
<AppQuickMenuModal :actions="quickMenuActions" />
|
||||
<AppScannerModal />
|
||||
<SidebarProvider :default-open="sidebarState">
|
||||
@@ -41,7 +42,7 @@
|
||||
v-for="btn in dropdown"
|
||||
:key="btn.id"
|
||||
class="group cursor-pointer text-lg"
|
||||
@click="openDialog(btn.dialogId)"
|
||||
@click="openDialog(btn.dialogId as NoParamDialogIDs)"
|
||||
>
|
||||
{{ btn.name.value }}
|
||||
<Shortcut
|
||||
@@ -78,7 +79,7 @@
|
||||
'text-nowrap': typeof locale === 'string' && locale.startsWith('zh-'),
|
||||
}"
|
||||
:tooltip="$t('menu.scanner')"
|
||||
@click.prevent="openDialog('scanner')"
|
||||
@click.prevent="openDialog(DialogID.Scanner)"
|
||||
>
|
||||
<MdiQrcodeScan />
|
||||
<span>{{ $t("menu.scanner") }}</span>
|
||||
@@ -209,6 +210,7 @@
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import { DialogID, type NoParamDialogIDs, type OptionalDialogIDs } from "~/components/ui/dialog-provider/utils";
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const username = computed(() => authCtx.user?.name || "User");
|
||||
@@ -249,7 +251,7 @@
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ video: true })
|
||||
.then(() => {
|
||||
openDialog("scanner");
|
||||
openDialog(DialogID.Scanner);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
@@ -263,24 +265,31 @@
|
||||
// Preload currency format
|
||||
useFormatCurrency();
|
||||
|
||||
const dropdown = [
|
||||
type DropdownItem = {
|
||||
id: number;
|
||||
name: ComputedRef<string>;
|
||||
shortcut: string;
|
||||
dialogId: NoParamDialogIDs | OptionalDialogIDs;
|
||||
};
|
||||
|
||||
const dropdown: DropdownItem[] = [
|
||||
{
|
||||
id: 0,
|
||||
name: computed(() => t("menu.create_item")),
|
||||
shortcut: "Shift+1",
|
||||
dialogId: "create-item",
|
||||
dialogId: DialogID.CreateItem,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: computed(() => t("menu.create_location")),
|
||||
shortcut: "Shift+3",
|
||||
dialogId: "create-location",
|
||||
dialogId: DialogID.CreateLocation,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: computed(() => t("menu.create_label")),
|
||||
shortcut: "Shift+2",
|
||||
dialogId: "create-label",
|
||||
dialogId: DialogID.CreateLabel,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -334,7 +343,7 @@
|
||||
const quickMenuActions = reactive([
|
||||
...dropdown.map(v => ({
|
||||
text: computed(() => v.name.value),
|
||||
dialogId: v.dialogId,
|
||||
dialogId: v.dialogId as NoParamDialogIDs,
|
||||
shortcut: v.shortcut.split("+")[1],
|
||||
type: "create" as const,
|
||||
})),
|
||||
|
||||
@@ -153,6 +153,26 @@ export class ItemsApi extends BaseAPI {
|
||||
return resp;
|
||||
}
|
||||
|
||||
duplicate(
|
||||
id: string,
|
||||
options: {
|
||||
copyMaintenance?: boolean;
|
||||
copyAttachments?: boolean;
|
||||
copyCustomFields?: boolean;
|
||||
copyPrefix?: string;
|
||||
} = {}
|
||||
) {
|
||||
return this.http.post<typeof options, ItemOut>({
|
||||
url: route(`/items/${id}/duplicate`),
|
||||
body: {
|
||||
copyMaintenance: options.copyMaintenance,
|
||||
copyAttachments: options.copyAttachments,
|
||||
copyCustomFields: options.copyCustomFields,
|
||||
copyPrefix: options.copyPrefix,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
import(file: File | Blob) {
|
||||
const formData = new FormData();
|
||||
formData.append("csv", file);
|
||||
|
||||
8
frontend/lib/api/classes/product.ts
Normal file
8
frontend/lib/api/classes/product.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { BaseAPI, route } from "../base";
|
||||
import type { BarcodeProduct } from "../types/data-contracts";
|
||||
|
||||
export class ProductAPI extends BaseAPI {
|
||||
searchFromBarcode(productEAN: string) {
|
||||
return this.http.get<BarcodeProduct[]>({ url: route(`/products/search-from-barcode`, { productEAN }) });
|
||||
}
|
||||
}
|
||||
@@ -451,6 +451,19 @@ export interface EntUserEdges {
|
||||
notifiers: EntNotifier[];
|
||||
}
|
||||
|
||||
export interface BarcodeProduct {
|
||||
barcode: string;
|
||||
imageBase64: string;
|
||||
imageURL: string;
|
||||
item: ItemCreate;
|
||||
manufacturer: string;
|
||||
/** Identifications */
|
||||
modelNumber: string;
|
||||
/** Extras */
|
||||
notes: string;
|
||||
search_engine_name: string;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
createdAt: Date | string;
|
||||
currency: string;
|
||||
@@ -631,7 +644,7 @@ export interface ItemUpdate {
|
||||
|
||||
export interface LabelCreate {
|
||||
color: string;
|
||||
/** @maxLength 255 */
|
||||
/** @maxLength 1000 */
|
||||
description: string;
|
||||
/**
|
||||
* @minLength 1
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AssetsApi } from "./classes/assets";
|
||||
import { ReportsAPI } from "./classes/reports";
|
||||
import { NotifiersAPI } from "./classes/notifiers";
|
||||
import { MaintenanceAPI } from "./classes/maintenance";
|
||||
import { ProductAPI } from "./classes/product";
|
||||
import type { Requests } from "~~/lib/requests";
|
||||
|
||||
export class UserClient extends BaseAPI {
|
||||
@@ -24,6 +25,7 @@ export class UserClient extends BaseAPI {
|
||||
assets: AssetsApi;
|
||||
reports: ReportsAPI;
|
||||
notifiers: NotifiersAPI;
|
||||
products: ProductAPI;
|
||||
|
||||
constructor(requests: Requests, attachmentToken: string) {
|
||||
super(requests, attachmentToken);
|
||||
@@ -39,6 +41,7 @@ export class UserClient extends BaseAPI {
|
||||
this.assets = new AssetsApi(requests);
|
||||
this.reports = new ReportsAPI(requests);
|
||||
this.notifiers = new NotifiersAPI(requests);
|
||||
this.products = new ProductAPI(requests);
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
@@ -153,3 +153,19 @@ export const themes: ThemeOption[] = [
|
||||
value: "winter",
|
||||
},
|
||||
];
|
||||
|
||||
export const darkThemes: DaisyTheme[] = [
|
||||
"synthwave",
|
||||
"retro",
|
||||
"cyberpunk",
|
||||
"valentine",
|
||||
"halloween",
|
||||
"forest",
|
||||
"aqua",
|
||||
"black",
|
||||
"luxury",
|
||||
"dracula",
|
||||
"business",
|
||||
"night",
|
||||
"coffee",
|
||||
];
|
||||
|
||||
8
frontend/locales/.README FIRST.md
Normal file
8
frontend/locales/.README FIRST.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# DO NOT EDIT FILES HERE DIRECTLY UNLESS ADDING KEYS!
|
||||
|
||||
Please do not edit the files here directly unless your adding new translation keys to the en.json file.
|
||||
|
||||
All translations should be added according to [our documentation](https://homebox.software/en/contribute/get-started.html#translations) through our [weblate instance](https://translate.sysadminsmedia.com).
|
||||
This helps ensure that our translations are consistent and effective.
|
||||
|
||||
PRs that modify these files directly will be closed without merge.
|
||||
@@ -1,20 +1,9 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"create_modal": {
|
||||
"createAndAddAnother": "",
|
||||
"enter": "",
|
||||
"shift": ""
|
||||
},
|
||||
"import_dialog": {
|
||||
"change_warning": "Način unosa podataka s postojećim import_refs se promjenio. Ako je import_ref prisutan u CSV fajlu,\npredmet će se ažurirati s vrijednostima iz CSV fajla.",
|
||||
"description": "",
|
||||
"title": "Importuj CSV fajl",
|
||||
"toast": {
|
||||
"import_failed": "",
|
||||
"import_success": "",
|
||||
"please_select_file": ""
|
||||
}
|
||||
"title": "Importuj CSV fajl"
|
||||
},
|
||||
"outdated": {
|
||||
"current_version": "Trenutna verzija",
|
||||
@@ -24,11 +13,6 @@
|
||||
"new_version_available_link": "Klikni ovdje za pregled bilješke o izdanju"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"password": {
|
||||
"toggle_show": ""
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"copy_text": {
|
||||
"documentation": "dokumentacija",
|
||||
@@ -49,9 +33,6 @@
|
||||
"minute": "minuta",
|
||||
"minutes": "minuta",
|
||||
"months": "mjeseci",
|
||||
"next-month": "",
|
||||
"next-week": "",
|
||||
"next-year": "",
|
||||
"second": "sekunda",
|
||||
"seconds": "sekundi",
|
||||
"tomorrow": "sutra",
|
||||
@@ -59,553 +40,79 @@
|
||||
"weeks": "sedmica/e",
|
||||
"years": "godine/a",
|
||||
"yesterday": "jučer"
|
||||
},
|
||||
"label_maker": {
|
||||
"browser_print": "",
|
||||
"confirm_description": "",
|
||||
"download": "",
|
||||
"print": "",
|
||||
"server_print": "",
|
||||
"titles": "",
|
||||
"toast": {
|
||||
"load_status_failed": "",
|
||||
"print_failed": "",
|
||||
"print_success": ""
|
||||
}
|
||||
},
|
||||
"page_qr_code": {
|
||||
"page_url": "",
|
||||
"qr_tooltip": ""
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": ""
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"attachments_list": {
|
||||
"download": "",
|
||||
"open_new_tab": ""
|
||||
},
|
||||
"create_modal": {
|
||||
"delete_photo": "",
|
||||
"item_description": "",
|
||||
"item_name": "",
|
||||
"item_photo": "",
|
||||
"item_quantity": "",
|
||||
"parent_item": "",
|
||||
"rotate_photo": "",
|
||||
"set_as_primary_photo": "",
|
||||
"title": "",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": "",
|
||||
"failed_load_parent": "",
|
||||
"no_canvas_support": "",
|
||||
"please_select_location": "",
|
||||
"rotate_failed": "",
|
||||
"rotate_process_failed": "",
|
||||
"some_photos_failed": "",
|
||||
"upload_failed": "",
|
||||
"upload_success": "",
|
||||
"uploading_photos": ""
|
||||
},
|
||||
"upload_photos": "",
|
||||
"uploaded": ""
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "",
|
||||
"placeholder": "",
|
||||
"search_placeholder": ""
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Karta",
|
||||
"items": "Predmeti",
|
||||
"no_items": "",
|
||||
"table": "Tabela"
|
||||
},
|
||||
"table": {
|
||||
"headers": "Zaglavlje",
|
||||
"page": "Stranica",
|
||||
"rows_per_page": "",
|
||||
"table_settings": "",
|
||||
"view_item": ""
|
||||
"page": "Stranica"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"label_description": "",
|
||||
"label_name": "",
|
||||
"title": "",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": "",
|
||||
"label_name_too_long": ""
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"select_labels": ""
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"location_description": "",
|
||||
"location_name": "",
|
||||
"title": "",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": ""
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"no_location_found": "",
|
||||
"parent_location": "",
|
||||
"search_location": "",
|
||||
"select_location": ""
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": ""
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
"no_results": "",
|
||||
"shortcut_hint": ""
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"add": "Dodaj",
|
||||
"archived": "Arhivirano",
|
||||
"build": "",
|
||||
"cancel": "Poništi",
|
||||
"confirm": "Potvrdi",
|
||||
"create": "Kreiraj",
|
||||
"create_and_add": "",
|
||||
"create_subitem": "",
|
||||
"created": "Kreirano",
|
||||
"delete": "Obriši",
|
||||
"delete_confirm": "",
|
||||
"demo_instance": "",
|
||||
"details": "Detalji",
|
||||
"duplicate": "Dupliciraj",
|
||||
"edit": "Izmjeni",
|
||||
"email": "Email",
|
||||
"follow_dev": "",
|
||||
"footer": {
|
||||
"api_link": "",
|
||||
"version_link": ""
|
||||
},
|
||||
"github": "",
|
||||
"insured": "Osigurano",
|
||||
"items": "Predmeti",
|
||||
"join_discord": "",
|
||||
"labels": "Etikete",
|
||||
"loading": "",
|
||||
"locations": "Lokacije",
|
||||
"maintenance": "Održavanje",
|
||||
"name": "Ime",
|
||||
"navigate": "Navigiraj",
|
||||
"password": "Šifra",
|
||||
"quantity": "Količina",
|
||||
"read_docs": "",
|
||||
"return_home": "",
|
||||
"save": "Sačuvaj",
|
||||
"search": "Pretraži",
|
||||
"sign_out": "",
|
||||
"submit": "",
|
||||
"unknown": "",
|
||||
"update": "",
|
||||
"updating": "",
|
||||
"value": "",
|
||||
"version": "",
|
||||
"welcome": ""
|
||||
},
|
||||
"home": {
|
||||
"labels": "",
|
||||
"quick_statistics": "",
|
||||
"recently_added": "",
|
||||
"storage_locations": "",
|
||||
"total_items": "",
|
||||
"total_labels": "",
|
||||
"total_locations": "",
|
||||
"total_value": ""
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "",
|
||||
"dont_join_group": "",
|
||||
"joining_group": "",
|
||||
"login": "",
|
||||
"register": "",
|
||||
"remember_me": "",
|
||||
"set_email": "",
|
||||
"set_name": "",
|
||||
"set_password": "",
|
||||
"tagline": "",
|
||||
"title": "",
|
||||
"toast": {
|
||||
"invalid_email": "",
|
||||
"invalid_email_password": "",
|
||||
"login_success": "",
|
||||
"problem_registering": "",
|
||||
"user_registered": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"add": "",
|
||||
"advanced": "",
|
||||
"archived": "",
|
||||
"asset_id": "",
|
||||
"associated_with_multiple": "",
|
||||
"attachment": "",
|
||||
"attachments": "",
|
||||
"changes_persisted_immediately": "",
|
||||
"created_at": "",
|
||||
"custom_fields": "",
|
||||
"delete_attachment_confirm": "",
|
||||
"delete_item_confirm": "",
|
||||
"description": "",
|
||||
"details": "",
|
||||
"drag_and_drop": "",
|
||||
"edit": {
|
||||
"edit_attachment_dialog": {
|
||||
"attachment_title": "",
|
||||
"attachment_type": "",
|
||||
"primary_photo": "",
|
||||
"primary_photo_sub": "",
|
||||
"select_type": "",
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"edit_details": "",
|
||||
"field_selector": "",
|
||||
"field_value": "",
|
||||
"first": "",
|
||||
"include_archive": "",
|
||||
"insured": "",
|
||||
"invalid_asset_id": "",
|
||||
"last": "",
|
||||
"lifetime_warranty": "",
|
||||
"location": "",
|
||||
"manual": "",
|
||||
"manuals": "",
|
||||
"manufacturer": "",
|
||||
"model_number": "",
|
||||
"name": "",
|
||||
"negate_labels": "",
|
||||
"next_page": "",
|
||||
"no_attachments": "",
|
||||
"no_results": "",
|
||||
"notes": "",
|
||||
"only_with_photo": "",
|
||||
"only_without_photo": "",
|
||||
"options": "",
|
||||
"order_by": "",
|
||||
"pages": "",
|
||||
"parent_item": "",
|
||||
"photo": "",
|
||||
"photos": "",
|
||||
"prev_page": "",
|
||||
"purchase_date": "",
|
||||
"purchase_details": "",
|
||||
"purchase_price": "",
|
||||
"purchased_from": "",
|
||||
"quantity": "",
|
||||
"query_id": "",
|
||||
"receipt": "",
|
||||
"receipts": "",
|
||||
"reset_search": "",
|
||||
"results": "",
|
||||
"select_field": "",
|
||||
"serial_number": "",
|
||||
"show_advanced_view_options": "",
|
||||
"sold_at": "",
|
||||
"sold_details": "",
|
||||
"sold_price": "",
|
||||
"sold_to": "",
|
||||
"sync_child_locations": "",
|
||||
"tip_1": "",
|
||||
"tip_2": "",
|
||||
"tip_3": "",
|
||||
"tips": "",
|
||||
"tips_sub": "",
|
||||
"toast": {
|
||||
"asset_not_found": "",
|
||||
"attachment_deleted": "",
|
||||
"attachment_updated": "",
|
||||
"attachment_uploaded": "",
|
||||
"child_items_location_no_longer_synced": "",
|
||||
"child_items_location_synced": "",
|
||||
"child_location_desync": "",
|
||||
"error_loading_parent_data": "",
|
||||
"failed_adjust_quantity": "",
|
||||
"failed_delete_attachment": "",
|
||||
"failed_delete_item": "",
|
||||
"failed_duplicate_item": "",
|
||||
"failed_load_asset": "",
|
||||
"failed_load_item": "",
|
||||
"failed_load_items": "",
|
||||
"failed_save": "",
|
||||
"failed_save_no_location": "",
|
||||
"failed_search_items": "",
|
||||
"failed_update_attachment": "",
|
||||
"failed_upload_attachment": "",
|
||||
"item_deleted": "",
|
||||
"item_saved": "",
|
||||
"quantity_cannot_negative": "",
|
||||
"sync_child_location": ""
|
||||
},
|
||||
"updated_at": "",
|
||||
"warranty": "",
|
||||
"warranty_details": "",
|
||||
"warranty_expires": ""
|
||||
},
|
||||
"labels": {
|
||||
"label_delete_confirm": "",
|
||||
"no_results": "",
|
||||
"toast": {
|
||||
"failed_delete_label": "",
|
||||
"failed_load_label": "",
|
||||
"failed_update_label": "",
|
||||
"label_deleted": "",
|
||||
"label_updated": ""
|
||||
},
|
||||
"update_label": ""
|
||||
},
|
||||
"languages": {
|
||||
"ca": "",
|
||||
"cs-CZ": "",
|
||||
"da-DK": "",
|
||||
"de": "",
|
||||
"en": "",
|
||||
"es": "",
|
||||
"fi-FI": "",
|
||||
"fr": "",
|
||||
"hu": "",
|
||||
"id-ID": "",
|
||||
"it": "",
|
||||
"ja-JP": "",
|
||||
"ko-KR": "",
|
||||
"lb-LU": "",
|
||||
"lt-LT": "",
|
||||
"nb-NO": "",
|
||||
"nl": "",
|
||||
"pl": "",
|
||||
"pt-BR": "",
|
||||
"pt-PT": "",
|
||||
"ro-RO": "",
|
||||
"ru": "",
|
||||
"sk-SK": "",
|
||||
"sl": "",
|
||||
"sq-AL": "",
|
||||
"sv": "",
|
||||
"ta-IN": "",
|
||||
"th-TH": "",
|
||||
"tr": "",
|
||||
"uk-UA": "",
|
||||
"zh-CN": "",
|
||||
"zh-HK": "",
|
||||
"zh-MO": "",
|
||||
"zh-TW": ""
|
||||
},
|
||||
"locations": {
|
||||
"child_locations": "",
|
||||
"collapse_tree": "",
|
||||
"expand_tree": "",
|
||||
"location_items_delete_confirm": "",
|
||||
"no_results": "",
|
||||
"toast": {
|
||||
"failed_delete_location": "",
|
||||
"failed_load_location": "",
|
||||
"failed_update_location": "",
|
||||
"location_deleted": "",
|
||||
"location_updated": ""
|
||||
},
|
||||
"update_location": ""
|
||||
"search": "Pretraži"
|
||||
},
|
||||
"maintenance": {
|
||||
"filter": {
|
||||
"both": "",
|
||||
"completed": "",
|
||||
"scheduled": ""
|
||||
},
|
||||
"list": {
|
||||
"complete": "",
|
||||
"create_first": "",
|
||||
"delete": "Obriši",
|
||||
"duplicate": "Dupliciraj",
|
||||
"edit": "Izmjeni",
|
||||
"new": ""
|
||||
"edit": "Izmjeni"
|
||||
},
|
||||
"modal": {
|
||||
"completed_date": "",
|
||||
"cost": "",
|
||||
"delete_confirmation": "",
|
||||
"edit_action": "",
|
||||
"edit_title": "",
|
||||
"entry_name": "",
|
||||
"new_action": "",
|
||||
"new_title": "",
|
||||
"notes": "Bilješke",
|
||||
"scheduled_date": ""
|
||||
},
|
||||
"monthly_average": "",
|
||||
"toast": {
|
||||
"failed_to_create": "",
|
||||
"failed_to_delete": "",
|
||||
"failed_to_update": ""
|
||||
},
|
||||
"total_cost": "",
|
||||
"total_entries": ""
|
||||
"notes": "Bilješke"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"create_item": "",
|
||||
"create_label": "",
|
||||
"create_location": "",
|
||||
"home": "",
|
||||
"locations": "",
|
||||
"maintenance": "",
|
||||
"profile": "Profil",
|
||||
"scanner": "",
|
||||
"search": "",
|
||||
"tools": ""
|
||||
"profile": "Profil"
|
||||
},
|
||||
"profile": {
|
||||
"active": "",
|
||||
"change_password": "",
|
||||
"currency_format": "",
|
||||
"current_password": "",
|
||||
"delete_account": "",
|
||||
"delete_account_confirm": "",
|
||||
"delete_account_sub": "",
|
||||
"delete_notifier_confirm": "",
|
||||
"display_legacy_header": "",
|
||||
"enabled": "Uključeno",
|
||||
"example": "Primjer",
|
||||
"gen_invite": "Generiši link pozivnicu",
|
||||
"group_settings": "Postavke grupe",
|
||||
"group_settings_sub": "",
|
||||
"inactive": "",
|
||||
"language": "",
|
||||
"new_password": "",
|
||||
"no_notifiers": "",
|
||||
"no_override": "",
|
||||
"notifier_modal": "",
|
||||
"notifiers": "",
|
||||
"notifiers_sub": "",
|
||||
"override_locale": "",
|
||||
"test": "Test",
|
||||
"theme_settings": "",
|
||||
"theme_settings_sub": "",
|
||||
"toast": {
|
||||
"account_deleted": "",
|
||||
"failed_change_password": "",
|
||||
"failed_create_notifier": "",
|
||||
"failed_delete_account": "",
|
||||
"failed_delete_notifier": "",
|
||||
"failed_get_currencies": "",
|
||||
"failed_test_notifier": "",
|
||||
"failed_update_group": "",
|
||||
"failed_update_notifier": "",
|
||||
"group_updated": "",
|
||||
"notifier_test_success": "",
|
||||
"password_changed": ""
|
||||
},
|
||||
"update_group": "",
|
||||
"update_language": "",
|
||||
"url": "",
|
||||
"user_profile": "",
|
||||
"user_profile_sub": ""
|
||||
},
|
||||
"reports": {
|
||||
"label_generator": {
|
||||
"asset_end": "",
|
||||
"asset_start": "",
|
||||
"base_url": "",
|
||||
"bordered_labels": "",
|
||||
"generate_page": "",
|
||||
"input_placeholder": "",
|
||||
"instruction_1": "",
|
||||
"instruction_2": "",
|
||||
"instruction_3": "",
|
||||
"label_height": "",
|
||||
"label_width": "",
|
||||
"measure_type": "",
|
||||
"page_bottom_padding": "",
|
||||
"page_height": "",
|
||||
"page_left_padding": "",
|
||||
"page_right_padding": "",
|
||||
"page_top_padding": "",
|
||||
"page_width": "",
|
||||
"qr_code_example": "",
|
||||
"tip_1": "",
|
||||
"tip_2": "",
|
||||
"tip_3": "",
|
||||
"tips": "",
|
||||
"title": "",
|
||||
"toast": {
|
||||
"page_too_small_card": ""
|
||||
}
|
||||
}
|
||||
"test": "Test"
|
||||
},
|
||||
"scanner": {
|
||||
"error": "",
|
||||
"invalid_url": "",
|
||||
"no_sources": "",
|
||||
"permission_denied": "",
|
||||
"select_video_source": "",
|
||||
"title": "Skener",
|
||||
"unsupported": ""
|
||||
"title": "Skener"
|
||||
},
|
||||
"tools": {
|
||||
"actions": "",
|
||||
"actions_set": {
|
||||
"ensure_ids": "",
|
||||
"ensure_ids_button": "",
|
||||
"ensure_ids_confirm": "",
|
||||
"ensure_ids_sub": "",
|
||||
"ensure_import_refs": "",
|
||||
"ensure_import_refs_button": "",
|
||||
"ensure_import_refs_sub": "",
|
||||
"set_primary_photo": "",
|
||||
"set_primary_photo_button": "",
|
||||
"set_primary_photo_confirm": "",
|
||||
"set_primary_photo_sub": "",
|
||||
"zero_datetimes": "",
|
||||
"zero_datetimes_button": "",
|
||||
"zero_datetimes_confirm": "",
|
||||
"zero_datetimes_sub": ""
|
||||
},
|
||||
"actions_sub": "",
|
||||
"import_export": "Imortovanje/Eksportovanje",
|
||||
"import_export_set": {
|
||||
"export": "Izvoz inventara",
|
||||
"export_button": "Izvoz inventara",
|
||||
"export_sub": "",
|
||||
"import": "",
|
||||
"import_button": "",
|
||||
"import_ref_confirm": "",
|
||||
"import_sub": ""
|
||||
"export_button": "Izvoz inventara"
|
||||
},
|
||||
"import_export_sub": "",
|
||||
"reports": "Izvještaji",
|
||||
"reports_set": {
|
||||
"asset_labels": "",
|
||||
"asset_labels_button": "",
|
||||
"asset_labels_sub": "",
|
||||
"bill_of_materials": "Popis materijala",
|
||||
"bill_of_materials_button": "Generiši popis materijala",
|
||||
"bill_of_materials_sub": ""
|
||||
},
|
||||
"reports_sub": "",
|
||||
"toast": {
|
||||
"asset_success": "",
|
||||
"failed_ensure_ids": "",
|
||||
"failed_ensure_import_refs": "",
|
||||
"failed_set_primary_photos": "",
|
||||
"failed_zero_datetimes": ""
|
||||
"bill_of_materials_button": "Generiši popis materijala"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
"app": {
|
||||
"create_modal": {
|
||||
"createAndAddAnother": "Utilitza {shiftKey} + {enterKey} per crear i afegir-ne un altre.",
|
||||
"enter": "Entrar",
|
||||
"shift": ""
|
||||
"enter": "Entrar"
|
||||
},
|
||||
"import_dialog": {
|
||||
"change_warning": "El comportament de les importacions amb import_refs existents ha canviat. Si hi ha un import_refs al fitxer CSV, \nl'article s'actualitzarà amb els valors del fitxer CSV.",
|
||||
@@ -24,18 +23,7 @@
|
||||
"new_version_available_link": "Fes clic aquí per veure les notes de la versió"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"password": {
|
||||
"toggle_show": ""
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"copy_text": {
|
||||
"documentation": "",
|
||||
"failed_to_copy": "",
|
||||
"https_required": "",
|
||||
"learn_more": ""
|
||||
},
|
||||
"date_time": {
|
||||
"ago": "fa {0}",
|
||||
"days": "dies",
|
||||
@@ -61,62 +49,24 @@
|
||||
"yesterday": "ahir"
|
||||
},
|
||||
"label_maker": {
|
||||
"browser_print": "",
|
||||
"confirm_description": "",
|
||||
"download": "",
|
||||
"print": "",
|
||||
"server_print": "Imprimeix al servidor",
|
||||
"titles": "Etiquetes",
|
||||
"toast": {
|
||||
"load_status_failed": "Fallat en carregar l'estat",
|
||||
"print_failed": "",
|
||||
"print_success": ""
|
||||
"load_status_failed": "Fallat en carregar l'estat"
|
||||
}
|
||||
},
|
||||
"page_qr_code": {
|
||||
"page_url": "URL de la pàgina",
|
||||
"qr_tooltip": ""
|
||||
"page_url": "URL de la pàgina"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Força de la contrasenya"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"attachments_list": {
|
||||
"download": "",
|
||||
"open_new_tab": ""
|
||||
},
|
||||
"create_modal": {
|
||||
"delete_photo": "",
|
||||
"item_description": "Descripció de l'article",
|
||||
"item_name": "Nom de l'article",
|
||||
"item_photo": "",
|
||||
"item_quantity": "",
|
||||
"parent_item": "",
|
||||
"rotate_photo": "",
|
||||
"set_as_primary_photo": "",
|
||||
"title": "Crea un article",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": "",
|
||||
"failed_load_parent": "",
|
||||
"no_canvas_support": "",
|
||||
"please_select_location": "",
|
||||
"rotate_failed": "",
|
||||
"rotate_process_failed": "",
|
||||
"some_photos_failed": "",
|
||||
"upload_failed": "",
|
||||
"upload_success": "",
|
||||
"uploading_photos": ""
|
||||
},
|
||||
"upload_photos": "",
|
||||
"uploaded": ""
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "",
|
||||
"placeholder": "",
|
||||
"search_placeholder": ""
|
||||
"title": "Crea un article"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
@@ -126,11 +76,8 @@
|
||||
"table": "Taula"
|
||||
},
|
||||
"table": {
|
||||
"headers": "",
|
||||
"page": "Pàgina",
|
||||
"rows_per_page": "Files per pàgina",
|
||||
"table_settings": "",
|
||||
"view_item": ""
|
||||
"rows_per_page": "Files per pàgina"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -138,87 +85,50 @@
|
||||
"create_modal": {
|
||||
"label_description": "Descripció de l'etiqueta",
|
||||
"label_name": "Nom de l'etiqueta",
|
||||
"title": "Crea una etiqueta",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": "",
|
||||
"label_name_too_long": ""
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"select_labels": ""
|
||||
"title": "Crea una etiqueta"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"location_description": "Descripció de la ubicació",
|
||||
"location_name": "Nom de la ubicació",
|
||||
"title": "Crea una ubicació",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": ""
|
||||
}
|
||||
"title": "Crea una ubicació"
|
||||
},
|
||||
"selector": {
|
||||
"no_location_found": "",
|
||||
"parent_location": "Ubicació pare",
|
||||
"search_location": "",
|
||||
"select_location": ""
|
||||
"parent_location": "Ubicació pare"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "No hi ha ubicacions disponibles. Afegiu ubicacions amb el botó\n `<`span class=\"link-primary\"`>`Crea`<`/span`>` a la barra de navegació."
|
||||
"no_locations": "No hi ha ubicacions disponibles. Afegiu ubicacions amb el botó\n '<span class=\"link-primary\">'Crea'</span>' a la barra de navegació."
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
"no_results": "",
|
||||
"shortcut_hint": ""
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"add": "Afegeix",
|
||||
"archived": "",
|
||||
"build": "Construcció: { build }",
|
||||
"cancel": "",
|
||||
"confirm": "Confirma",
|
||||
"create": "Crea",
|
||||
"create_and_add": "Crea i afegeix-ne un altre",
|
||||
"create_subitem": "",
|
||||
"created": "Creat",
|
||||
"delete": "Esborra",
|
||||
"delete_confirm": "",
|
||||
"demo_instance": "",
|
||||
"details": "Detalls",
|
||||
"duplicate": "Duplica",
|
||||
"edit": "Edita",
|
||||
"email": "Correu electrònic",
|
||||
"follow_dev": "Segueix al desenvolupador",
|
||||
"footer": {
|
||||
"api_link": "",
|
||||
"version_link": ""
|
||||
},
|
||||
"github": "Projecte de GitHub",
|
||||
"insured": "",
|
||||
"items": "Articles",
|
||||
"join_discord": "Uniu-vos a Discord",
|
||||
"labels": "Etiquetes",
|
||||
"loading": "",
|
||||
"locations": "Ubicacions",
|
||||
"maintenance": "Manteniment",
|
||||
"name": "Nom",
|
||||
"navigate": "",
|
||||
"password": "Contrasenya",
|
||||
"quantity": "",
|
||||
"read_docs": "Llegiu la documentació",
|
||||
"return_home": "",
|
||||
"save": "Desa",
|
||||
"search": "Cerca",
|
||||
"sign_out": "Tanca la sessió",
|
||||
"submit": "Envia",
|
||||
"unknown": "",
|
||||
"update": "Actualitza",
|
||||
"updating": "",
|
||||
"value": "Valor",
|
||||
"version": "Versió {version}",
|
||||
"welcome": "Us donem la benvinguda, { username }"
|
||||
@@ -243,49 +153,27 @@
|
||||
"set_email": "Quin és el seu correu electrònic?",
|
||||
"set_name": "Com us dieu?",
|
||||
"set_password": "Definiu la contrasenya",
|
||||
"tagline": "Feu el seguiment, organitzeu i gestioneu les vostres coses.",
|
||||
"title": "",
|
||||
"toast": {
|
||||
"invalid_email": "",
|
||||
"invalid_email_password": "",
|
||||
"login_success": "",
|
||||
"problem_registering": "",
|
||||
"user_registered": ""
|
||||
}
|
||||
"tagline": "Feu el seguiment, organitzeu i gestioneu les vostres coses."
|
||||
},
|
||||
"items": {
|
||||
"add": "Afegeix",
|
||||
"advanced": "Mode avançat",
|
||||
"archived": "Arxivat",
|
||||
"asset_id": "ID de l'actiu",
|
||||
"associated_with_multiple": "",
|
||||
"attachment": "Adjunt",
|
||||
"attachments": "Documents adjunts",
|
||||
"changes_persisted_immediately": "Els canvis als fitxers adjunts es desaran immediatament",
|
||||
"created_at": "Creat a",
|
||||
"custom_fields": "Camps personalitzats",
|
||||
"delete_attachment_confirm": "",
|
||||
"delete_item_confirm": "",
|
||||
"description": "Descripció",
|
||||
"details": "Detalls",
|
||||
"drag_and_drop": "Arrossegueu i deixeu anar fitxers aquí o feu clic per seleccionar fitxers",
|
||||
"edit": {
|
||||
"edit_attachment_dialog": {
|
||||
"attachment_title": "",
|
||||
"attachment_type": "",
|
||||
"primary_photo": "",
|
||||
"primary_photo_sub": "",
|
||||
"select_type": "",
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"edit_details": "Edita els detalls",
|
||||
"field_selector": "Selector del camp",
|
||||
"field_value": "Valor del camp",
|
||||
"first": "Primer",
|
||||
"include_archive": "Inclou els articles arxivats",
|
||||
"insured": "Assegurat",
|
||||
"invalid_asset_id": "",
|
||||
"last": "Últim",
|
||||
"lifetime_warranty": "Garantia de per vida",
|
||||
"location": "Ubicació",
|
||||
@@ -296,11 +184,8 @@
|
||||
"name": "Nom",
|
||||
"negate_labels": "Nega les etiquetes seleccionades",
|
||||
"next_page": "Pàgina següent",
|
||||
"no_attachments": "",
|
||||
"no_results": "No s'ha trobat cap element",
|
||||
"notes": "Notes",
|
||||
"only_with_photo": "",
|
||||
"only_without_photo": "",
|
||||
"options": "Opcions",
|
||||
"order_by": "Ordena per",
|
||||
"pages": "Pàgina { page } de { totalPages }",
|
||||
@@ -318,88 +203,42 @@
|
||||
"receipts": "Factures",
|
||||
"reset_search": "Reinicia la cerca",
|
||||
"results": "{ total } resultats",
|
||||
"select_field": "",
|
||||
"serial_number": "Número de sèrie",
|
||||
"show_advanced_view_options": "Mostra les opcions avançades de visualització",
|
||||
"sold_at": "Venut a (lloc)",
|
||||
"sold_details": "Detalls de la venda",
|
||||
"sold_price": "Preu de venda",
|
||||
"sold_to": "Venut a",
|
||||
"sync_child_locations": "",
|
||||
"tip_1": "Els filtres d'ubicació i etiquetes utilitzen l'operació «O». Si se'n selecciona més d'un, \nnomés se'n requerirà un per a coincidència.",
|
||||
"tip_2": "Les cerques amb el prefix «#» sol·licitaran un ID d'un actiu (per exemple, «#000-001»)",
|
||||
"tip_3": "Els filtres de camp utilitzen l'operació «O». Si se'n selecciona més d'un, \nnomés se'n requerirà un per a coincidència.",
|
||||
"tips": "Consells",
|
||||
"tips_sub": "Consells de cerca",
|
||||
"toast": {
|
||||
"asset_not_found": "",
|
||||
"attachment_deleted": "",
|
||||
"attachment_updated": "",
|
||||
"attachment_uploaded": "",
|
||||
"child_items_location_no_longer_synced": "",
|
||||
"child_items_location_synced": "",
|
||||
"child_location_desync": "",
|
||||
"error_loading_parent_data": "",
|
||||
"failed_adjust_quantity": "",
|
||||
"failed_delete_attachment": "",
|
||||
"failed_delete_item": "",
|
||||
"failed_duplicate_item": "",
|
||||
"failed_load_asset": "",
|
||||
"failed_load_item": "",
|
||||
"failed_load_items": "",
|
||||
"failed_save": "",
|
||||
"failed_save_no_location": "",
|
||||
"failed_search_items": "",
|
||||
"failed_update_attachment": "",
|
||||
"failed_upload_attachment": "",
|
||||
"item_deleted": "",
|
||||
"item_saved": "",
|
||||
"quantity_cannot_negative": "",
|
||||
"sync_child_location": ""
|
||||
},
|
||||
"updated_at": "Actualitzat a",
|
||||
"warranty": "Garantia",
|
||||
"warranty_details": "Detalls de la garantia",
|
||||
"warranty_expires": "La garantia caduca"
|
||||
},
|
||||
"labels": {
|
||||
"label_delete_confirm": "",
|
||||
"no_results": "No s'han trobat etiquetes",
|
||||
"toast": {
|
||||
"failed_delete_label": "",
|
||||
"failed_load_label": "",
|
||||
"failed_update_label": "",
|
||||
"label_deleted": "",
|
||||
"label_updated": ""
|
||||
},
|
||||
"update_label": "Actualitza l'etiqueta"
|
||||
},
|
||||
"languages": {
|
||||
"ca": "Català",
|
||||
"cs-CZ": "",
|
||||
"de": "Alemany",
|
||||
"en": "Anglès",
|
||||
"es": "Castellà",
|
||||
"fi-FI": "",
|
||||
"fr": "Francès",
|
||||
"hu": "Hongarès",
|
||||
"id-ID": "",
|
||||
"it": "Italià",
|
||||
"ja-JP": "Japonès",
|
||||
"ko-KR": "",
|
||||
"lb-LU": "",
|
||||
"lt-LT": "",
|
||||
"nb-NO": "",
|
||||
"nl": "Neerlandès",
|
||||
"pl": "Polonès",
|
||||
"pt-BR": "Portuguès (Brasil)",
|
||||
"pt-PT": "Portuguès (Portugal)",
|
||||
"ru": "Rus",
|
||||
"sl": "Eslovè",
|
||||
"sq-AL": "",
|
||||
"sv": "Suec",
|
||||
"ta-IN": "",
|
||||
"th-TH": "",
|
||||
"tr": "Turc",
|
||||
"uk-UA": "Ucraïnès",
|
||||
"zh-CN": "Xinès (simplificat)",
|
||||
@@ -413,16 +252,7 @@
|
||||
"locations": {
|
||||
"child_locations": "Ubicacions filles",
|
||||
"collapse_tree": "Col·lapsa l'arbre",
|
||||
"expand_tree": "",
|
||||
"location_items_delete_confirm": "",
|
||||
"no_results": "No s'han trobat ubicacions",
|
||||
"toast": {
|
||||
"failed_delete_location": "",
|
||||
"failed_load_location": "",
|
||||
"failed_update_location": "",
|
||||
"location_deleted": "",
|
||||
"location_updated": ""
|
||||
},
|
||||
"update_location": "Actualitza ubicació"
|
||||
},
|
||||
"maintenance": {
|
||||
@@ -468,7 +298,6 @@
|
||||
"locations": "Ubicacions",
|
||||
"maintenance": "Manteniment",
|
||||
"profile": "Perfil",
|
||||
"scanner": "",
|
||||
"search": "Cerca",
|
||||
"tools": "Eines"
|
||||
},
|
||||
@@ -478,10 +307,7 @@
|
||||
"currency_format": "Format de moneda",
|
||||
"current_password": "Contrasenya actual",
|
||||
"delete_account": "Suprimeix el compte",
|
||||
"delete_account_confirm": "",
|
||||
"delete_account_sub": "Elimina el compte i totes les dades associades. Aquesta acció no es pot desfer.",
|
||||
"delete_notifier_confirm": "",
|
||||
"display_legacy_header": "",
|
||||
"enabled": "Habilitat",
|
||||
"example": "Exemple",
|
||||
"gen_invite": "Genera un enllaç d'invitació",
|
||||
@@ -491,121 +317,44 @@
|
||||
"language": "Idioma",
|
||||
"new_password": "Contrasenya nova",
|
||||
"no_notifiers": "No hi ha notificadors configurats",
|
||||
"no_override": "",
|
||||
"notifier_modal": "{ type, select, true {Edita} false {Crea} other {Altres}} Notificació",
|
||||
"notifiers": "Notificadors",
|
||||
"notifiers_sub": "Rebeu notificacions per als pròxims recordatoris de manteniment",
|
||||
"override_locale": "",
|
||||
"test": "Prova",
|
||||
"theme_settings": "Configuracions del tema",
|
||||
"theme_settings_sub": "La configuració del tema s'emmagatzema a l'emmagatzematge local del navegador. Podeu canviar el tema en qualsevol moment. \nSi teniu problemes per definir el tema, proveu d'actualitzar el navegador.",
|
||||
"toast": {
|
||||
"account_deleted": "",
|
||||
"failed_change_password": "",
|
||||
"failed_create_notifier": "",
|
||||
"failed_delete_account": "",
|
||||
"failed_delete_notifier": "",
|
||||
"failed_get_currencies": "",
|
||||
"failed_test_notifier": "",
|
||||
"failed_update_group": "",
|
||||
"failed_update_notifier": "",
|
||||
"group_updated": "",
|
||||
"notifier_test_success": "",
|
||||
"password_changed": ""
|
||||
},
|
||||
"update_group": "Actualitza el grup",
|
||||
"update_language": "Actualitza l'idioma",
|
||||
"url": "URL",
|
||||
"user_profile": "Perfil d'usuari",
|
||||
"user_profile_sub": "Convida usuaris i gestiona el compte."
|
||||
},
|
||||
"reports": {
|
||||
"label_generator": {
|
||||
"asset_end": "",
|
||||
"asset_start": "",
|
||||
"base_url": "",
|
||||
"bordered_labels": "",
|
||||
"generate_page": "",
|
||||
"input_placeholder": "",
|
||||
"instruction_1": "",
|
||||
"instruction_2": "",
|
||||
"instruction_3": "",
|
||||
"label_height": "",
|
||||
"label_width": "",
|
||||
"measure_type": "",
|
||||
"page_bottom_padding": "",
|
||||
"page_height": "",
|
||||
"page_left_padding": "",
|
||||
"page_right_padding": "",
|
||||
"page_top_padding": "",
|
||||
"page_width": "",
|
||||
"qr_code_example": "",
|
||||
"tip_1": "",
|
||||
"tip_2": "",
|
||||
"tip_3": "",
|
||||
"tips": "",
|
||||
"title": "",
|
||||
"toast": {
|
||||
"page_too_small_card": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"error": "",
|
||||
"invalid_url": "",
|
||||
"no_sources": "",
|
||||
"permission_denied": "",
|
||||
"select_video_source": "",
|
||||
"title": "",
|
||||
"unsupported": ""
|
||||
},
|
||||
"tools": {
|
||||
"actions": "Accions d'inventari",
|
||||
"actions_set": {
|
||||
"ensure_ids": "Assegura els identificadors de recursos",
|
||||
"ensure_ids_button": "Assegura els identificadors de recursos",
|
||||
"ensure_ids_confirm": "",
|
||||
"ensure_ids_sub": "",
|
||||
"ensure_import_refs": "Assegureu-vos d'importar les referències",
|
||||
"ensure_import_refs_button": "Assegureu-vos d'importar les referències",
|
||||
"ensure_import_refs_sub": "",
|
||||
"set_primary_photo": "Defineix la foto principal",
|
||||
"set_primary_photo_button": "Defineix la foto principal",
|
||||
"set_primary_photo_confirm": "",
|
||||
"set_primary_photo_sub": "",
|
||||
"zero_datetimes": "",
|
||||
"zero_datetimes_button": "",
|
||||
"zero_datetimes_confirm": "",
|
||||
"zero_datetimes_sub": ""
|
||||
"set_primary_photo_button": "Defineix la foto principal"
|
||||
},
|
||||
"actions_sub": "",
|
||||
"import_export": "Importa / Exporta",
|
||||
"import_export_set": {
|
||||
"export": "Exporta inventari",
|
||||
"export_button": "Exporta inventari",
|
||||
"export_sub": "Exporta el format CSV estàndard per a Homebox. S'exportaran tots els articles de l'inventari.",
|
||||
"import": "Importa inventari",
|
||||
"import_button": "Importa inventari",
|
||||
"import_ref_confirm": "",
|
||||
"import_sub": ""
|
||||
"import_button": "Importa inventari"
|
||||
},
|
||||
"import_export_sub": "Importa i exporta l'inventari amb un fitxer CSV. És útil per a migracions d'inventari a una nova instància de Homebox.",
|
||||
"reports": "Informes",
|
||||
"reports_set": {
|
||||
"asset_labels": "Etiquetes d'identificador de recurs",
|
||||
"asset_labels_button": "Generador d'etiquetes",
|
||||
"asset_labels_sub": "",
|
||||
"bill_of_materials": "Llista de materials",
|
||||
"bill_of_materials_button": "Genera llista de materials",
|
||||
"bill_of_materials_sub": ""
|
||||
"bill_of_materials_button": "Genera llista de materials"
|
||||
},
|
||||
"reports_sub": "Genera informes per a l'inventari",
|
||||
"toast": {
|
||||
"asset_success": "",
|
||||
"failed_ensure_ids": "",
|
||||
"failed_ensure_import_refs": "",
|
||||
"failed_set_primary_photos": "",
|
||||
"failed_zero_datetimes": ""
|
||||
}
|
||||
"reports_sub": "Genera informes per a l'inventari."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
"item_photo": "Fotografie položky 📷",
|
||||
"item_quantity": "Množství položek",
|
||||
"parent_item": "Nadřazená položka",
|
||||
"product_tooltip_input_barcode": "Automatické vyplnění ručně zadaným čárovým kódem",
|
||||
"product_tooltip_scan_barcode": "Automatické vyplnění čárovým kódem z 📷",
|
||||
"rotate_photo": "Otočit fotku",
|
||||
"set_as_primary_photo": "Nastavit jako { isPrimary, select, true {non-} false {} other {}}primární fotku",
|
||||
"title": "Vytvořit položku",
|
||||
@@ -115,15 +117,24 @@
|
||||
"some_photos_failed": "{count, plural, =0 {Žádná fotka k nahrání.} =1 {Nepodařilo se nahrát 1 fotografii.} other {Některé fotografie se nepodařilo nahrát.}}",
|
||||
"upload_failed": "Nepodařilo se nahrát fotku: { photoName }",
|
||||
"upload_success": "{count, plural, =0 {Nejsou nahrané žádné fotky.} =1 {Fotka byla úspěšně nahrána.} other {Všechny fotky byly úspěšně nahrány.}}",
|
||||
"uploading_photos": "{count, plural, =0 {Žádné fotky k nahrání} =1 {Nahrávání 1 fotky...} other {Nahrávání {count} fotek...}}"
|
||||
"uploading_photos": "{count, plural, =0 {Žádné fotky k nahrání} =1 {Nahrávání 1 fotky…} other {Nahrávání {count} fotek…}}"
|
||||
},
|
||||
"upload_photos": "Nahrát fotografie",
|
||||
"uploaded": "Fotka byla nahrána"
|
||||
},
|
||||
"product_import": {
|
||||
"barcode": "Čárový kód produktu",
|
||||
"db_source": "Zdroj DB",
|
||||
"error_exception": "Při načítání čárového kódu položky došlo k výjimce: ",
|
||||
"error_invalid_barcode": "Byl zadán neplatný čárový kód",
|
||||
"error_not_found": "Žádný produkt s daným čárovým kódem nebyl nalezen.",
|
||||
"search_item": "Vyhledat produkt",
|
||||
"title": "Importovat produkt"
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "Nebyly nalezeny žádné výsledky",
|
||||
"placeholder": "Vyberte...",
|
||||
"search_placeholder": "Pište pro vyhledávání..."
|
||||
"placeholder": "Vyberte…",
|
||||
"search_placeholder": "Pište pro vyhledávání…"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
@@ -176,7 +187,7 @@
|
||||
"select_location": "Vybrat lokaci"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "Nejsou dostupné žádné lokace. Přidejte nové lokace\npomocí tlačítka `<`span class=\"link-primary\"`>`Vytvořit`<`/span`>` na navigační liště."
|
||||
"no_locations": "Nejsou dostupné žádné lokace. Přidejte nové lokace\npomocí tlačítka <span class=\"link-primary\">Vytvořit</span> na navigační liště."
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
@@ -184,6 +195,9 @@
|
||||
"shortcut_hint": "Pomocí číselných tlačítek rychle vyberte akci."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"api_failure": "Volání backendového API selhalo: "
|
||||
},
|
||||
"global": {
|
||||
"add": "Přidat",
|
||||
"archived": "Archivované",
|
||||
@@ -204,14 +218,14 @@
|
||||
"follow_dev": "Sledovat vývojáře",
|
||||
"footer": {
|
||||
"api_link": "'<a href=\"https://homebox.software/en/api/\" target=\"_blank\">'API'</a>'",
|
||||
"version_link": "'<a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/v'{ version }\" target=\"_blank\"> Verze: { version } Sestavení: { build } '</a>'"
|
||||
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Verze: { version } Sestavení: { build } '</a>'"
|
||||
},
|
||||
"github": "GitHub projekt",
|
||||
"insured": "Pojištěné",
|
||||
"items": "Položky",
|
||||
"join_discord": "Připojte se na Discord",
|
||||
"labels": "Štítky",
|
||||
"loading": "Načítání...",
|
||||
"loading": "Načítání…",
|
||||
"locations": "Lokality",
|
||||
"maintenance": "Údržba",
|
||||
"name": "Jméno",
|
||||
@@ -559,6 +573,8 @@
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"barcode_detected_message": "byl zjištěn čárový kód produktu",
|
||||
"barcode_fetch_data": "Načíst produktová data",
|
||||
"error": "Při skenování došlo k chybě",
|
||||
"invalid_url": "Neplatná adresa URL čárového kódu",
|
||||
"no_sources": "Nejsou k dispozici žádné zdroje videa",
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
"components": {
|
||||
"app": {
|
||||
"create_modal": {
|
||||
"createAndAddAnother": "",
|
||||
"enter": "",
|
||||
"shift": ""
|
||||
"createAndAddAnother": "Brug {shiftKey} + {enterKey} til at oprette og tilføje en ny.",
|
||||
"enter": "Indtast",
|
||||
"shift": "Flytte"
|
||||
},
|
||||
"import_dialog": {
|
||||
"change_warning": "Adfærd for imports med eksisterende import_refs har ændret sig. Hvis en import_ref er tilstede i CSV filen,\nvil genstanden blive opdateret med værdierne fra CSV filen.",
|
||||
"description": "Importer en CSV fil som indeholder dine genstande, etiketter, og lokationer. Se dokumentation for mere information vedrørende\nden korrekte format.",
|
||||
"title": "Importer CSV Fil",
|
||||
"toast": {
|
||||
"import_failed": "",
|
||||
"import_success": "",
|
||||
"please_select_file": ""
|
||||
"import_failed": "Importen mislykkedes. Prøv igen senere.",
|
||||
"import_success": "Importen er gennemført!",
|
||||
"please_select_file": "Vælg venligst en fil, der skal importeres."
|
||||
}
|
||||
},
|
||||
"outdated": {
|
||||
@@ -24,9 +24,16 @@
|
||||
"new_version_available_link": "Klik her for at læse udgivelsesnoterne"
|
||||
}
|
||||
},
|
||||
"color_selector": {
|
||||
"clear": "Reset farve",
|
||||
"color": "Farve",
|
||||
"no_color": "Ingen farve",
|
||||
"no_color_selected": "Ingen farve valgt",
|
||||
"randomize": "Tilfældig farve"
|
||||
},
|
||||
"form": {
|
||||
"password": {
|
||||
"toggle_show": ""
|
||||
"toggle_show": "Slå adgangskode til/fra Vis"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
@@ -68,9 +75,9 @@
|
||||
"server_print": "Print på Server",
|
||||
"titles": "Labels",
|
||||
"toast": {
|
||||
"load_status_failed": "",
|
||||
"print_failed": "",
|
||||
"print_success": ""
|
||||
"load_status_failed": "Status kunne ikke indlæses",
|
||||
"print_failed": "Kunne ikke udskrive etiketten",
|
||||
"print_success": "Etiket udskrevet"
|
||||
}
|
||||
},
|
||||
"page_qr_code": {
|
||||
@@ -83,40 +90,49 @@
|
||||
},
|
||||
"item": {
|
||||
"attachments_list": {
|
||||
"download": "",
|
||||
"open_new_tab": ""
|
||||
"download": "Download",
|
||||
"open_new_tab": "Åbn i ny fane"
|
||||
},
|
||||
"create_modal": {
|
||||
"delete_photo": "",
|
||||
"delete_photo": "Slet billede",
|
||||
"item_description": "Genstandsbeskrivelse",
|
||||
"item_name": "Genstandsnavn",
|
||||
"item_photo": "Vare Foto 📷",
|
||||
"item_quantity": "",
|
||||
"parent_item": "",
|
||||
"rotate_photo": "",
|
||||
"set_as_primary_photo": "",
|
||||
"item_quantity": "Vare Antal",
|
||||
"parent_item": "Overordnet element",
|
||||
"product_tooltip_scan_barcode": "Fyld automatisk med stregkode fra 📷",
|
||||
"rotate_photo": "Roter foto",
|
||||
"set_as_primary_photo": "Sæt som { isPrimary, select, true {non-} false {} other {}}primært foto",
|
||||
"title": "Opret genstand",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": "",
|
||||
"failed_load_parent": "",
|
||||
"no_canvas_support": "",
|
||||
"please_select_location": "",
|
||||
"rotate_failed": "",
|
||||
"rotate_process_failed": "",
|
||||
"some_photos_failed": "",
|
||||
"upload_failed": "",
|
||||
"upload_success": "",
|
||||
"uploading_photos": ""
|
||||
"already_creating": "Opretter allerede et element",
|
||||
"create_failed": "Kunne ikke oprette elementet",
|
||||
"create_success": "Element oprettet",
|
||||
"failed_load_parent": "Kunne ikke indlæse overordnet element - vælg venligst manuelt",
|
||||
"no_canvas_support": "Din browser understøtter ikke canvas-handlinger",
|
||||
"please_select_location": "Vælg venligst en placering.",
|
||||
"rotate_failed": "Kunne ikke rotere billedet: { error }",
|
||||
"rotate_process_failed": "Kunne ikke behandle roteret billede",
|
||||
"some_photos_failed": "{count, plural, =0 {Ingen billeder at uploade.} =1 {1 billede kunne ikke uploades.} other {Nogle billeder kunne ikke uploades.}}",
|
||||
"upload_failed": "Kunne ikke uploade billede: { photoName }",
|
||||
"upload_success": "{count, plural, =0 {Ingen billeder uploadet.} =1 {Foto uploadet.} other {Alle billeder uploadet.}}",
|
||||
"uploading_photos": "{count, plural, =0 {Ingen billeder at uploade} =1 {Uploader 1 billede…} other {Uploader {count} billeder…}}"
|
||||
},
|
||||
"upload_photos": "Upload Billeder",
|
||||
"uploaded": ""
|
||||
"uploaded": "Uploadet billede"
|
||||
},
|
||||
"product_import": {
|
||||
"barcode": "Produkts stregkode",
|
||||
"db_source": "DB kilde",
|
||||
"error_invalid_barcode": "Ugyldig stregkode angivet",
|
||||
"error_not_found": "Intet produkt fundet med angivet stregkode.",
|
||||
"search_item": "Søg produkt",
|
||||
"title": "Importér produkt"
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "",
|
||||
"placeholder": "",
|
||||
"search_placeholder": ""
|
||||
"no_results": "Ingen resultater fundet",
|
||||
"placeholder": "Vælg…",
|
||||
"search_placeholder": "Skriv for at søge…"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
@@ -126,24 +142,25 @@
|
||||
"table": "Tabel"
|
||||
},
|
||||
"table": {
|
||||
"headers": "",
|
||||
"headers": "Overskrifter",
|
||||
"page": "Side",
|
||||
"rows_per_page": "Rækker per side",
|
||||
"table_settings": "Tabel Indstillinger",
|
||||
"view_item": ""
|
||||
"view_item": "Se vare"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"label_color": "Etiketfarve",
|
||||
"label_description": "Etiketbeskrivelse",
|
||||
"label_name": "Etiketnavn",
|
||||
"title": "Opret label",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": "",
|
||||
"label_name_too_long": ""
|
||||
"already_creating": "Allerede oprettet en etiket",
|
||||
"create_failed": "Kunne ikke oprette etiket",
|
||||
"create_success": "Etiket oprettet",
|
||||
"label_name_too_long": "Etiketnavnet må ikke være længere end 50 tegn"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
@@ -156,69 +173,72 @@
|
||||
"location_name": "Lokationsnavn",
|
||||
"title": "Opret lokation",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": ""
|
||||
"already_creating": "Allerede oprettet en lokation",
|
||||
"create_failed": "Kunne ikke oprette placering",
|
||||
"create_success": "Placering oprettet"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"no_location_found": "",
|
||||
"no_location_found": "Ingen placering fundet",
|
||||
"parent_location": "Forældrelokation",
|
||||
"search_location": "",
|
||||
"select_location": ""
|
||||
"search_location": "Søg efter placeringer",
|
||||
"select_location": "Vælg en placering"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "Ingen tilgængelige lokationer. Opret nye lokationer gennem\n`<`span class=\"link-primary\">`Opret`<`/span`>` knappen i navigationslinjen."
|
||||
"no_locations": "Ingen tilgængelige placeringer. Tilføj nye placeringer via knappen \n'<span class=\"link-primary\">'Opret'</span>' på navigationslinjen."
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
"no_results": "",
|
||||
"no_results": "Ingen resultater fundet.",
|
||||
"shortcut_hint": "Brug de numeriske taster til hurtigt at vælge en handling."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"api_failure": "Backend API kald fejlede: "
|
||||
},
|
||||
"global": {
|
||||
"add": "Tilføj",
|
||||
"archived": "",
|
||||
"archived": "Arkiveret",
|
||||
"build": "Build: { build }",
|
||||
"cancel": "",
|
||||
"cancel": "Ophæv",
|
||||
"confirm": "Bekræft",
|
||||
"create": "Opret",
|
||||
"create_and_add": "Opret og tilføj ny",
|
||||
"create_subitem": "",
|
||||
"create_subitem": "Opret underelement",
|
||||
"created": "Oprettet",
|
||||
"delete": "Slet",
|
||||
"delete_confirm": "",
|
||||
"demo_instance": "",
|
||||
"delete_confirm": "Er du sikker på, at du vil slette dette element? ",
|
||||
"demo_instance": "Dette er en demo-instans",
|
||||
"details": "Detaljer",
|
||||
"duplicate": "Dupliker",
|
||||
"edit": "Rediger",
|
||||
"email": "Email",
|
||||
"follow_dev": "Følg udvikleren",
|
||||
"footer": {
|
||||
"api_link": "",
|
||||
"version_link": ""
|
||||
"api_link": "'<a href=\"https://homebox.software/en/api/\" target=\"_blank\">'API'</a>'",
|
||||
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Version: { version } Build: { build } '</a>'"
|
||||
},
|
||||
"github": "GitHub projekt",
|
||||
"insured": "",
|
||||
"insured": "Forsikret",
|
||||
"items": "Genstande",
|
||||
"join_discord": "Deltag i vores Discord",
|
||||
"labels": "Etiketter",
|
||||
"loading": "",
|
||||
"loading": "Indlæser…",
|
||||
"locations": "Lokationer",
|
||||
"maintenance": "Opretholdelse",
|
||||
"name": "Navn",
|
||||
"navigate": "Naviger",
|
||||
"password": "Adgangskode",
|
||||
"quantity": "",
|
||||
"quantity": "Mængde",
|
||||
"read_docs": "Læs Docs",
|
||||
"return_home": "",
|
||||
"return_home": "Vend hjem",
|
||||
"save": "Gem",
|
||||
"search": "Søg",
|
||||
"sign_out": "Log ud",
|
||||
"submit": "Indsend",
|
||||
"unknown": "",
|
||||
"unknown": "Ukendt",
|
||||
"update": "Opdater",
|
||||
"updating": "",
|
||||
"updating": "Opdaterer",
|
||||
"value": "Værdi",
|
||||
"version": "Version: { version }",
|
||||
"welcome": "Velkommen, { username }"
|
||||
@@ -244,13 +264,13 @@
|
||||
"set_name": "Hvad hedder du?",
|
||||
"set_password": "Opret din adgangskode",
|
||||
"tagline": "Følg, Organiser, og Håndter dine Ting.",
|
||||
"title": "",
|
||||
"title": "Organiser og Tag dine ting",
|
||||
"toast": {
|
||||
"invalid_email": "",
|
||||
"invalid_email_password": "",
|
||||
"login_success": "",
|
||||
"problem_registering": "",
|
||||
"user_registered": ""
|
||||
"invalid_email": "Ugyldig e-mailadresse",
|
||||
"invalid_email_password": "Ugyldig e-mail eller adgangskode",
|
||||
"login_success": "Logget ind",
|
||||
"problem_registering": "Problem med at registrere bruger",
|
||||
"user_registered": "Bruger registreret"
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
@@ -258,25 +278,25 @@
|
||||
"advanced": "Avanceret",
|
||||
"archived": "Arkiveret",
|
||||
"asset_id": "Aktiv-id",
|
||||
"associated_with_multiple": "",
|
||||
"associated_with_multiple": "Dette aktiv-id er knyttet til flere varer",
|
||||
"attachment": "Vedhæftning",
|
||||
"attachments": "Vedhæftninger",
|
||||
"changes_persisted_immediately": "Ændringer af vedhæftede filer gemmes med det samme",
|
||||
"created_at": "Oprettet den",
|
||||
"custom_fields": "Brugerdefinerede felter",
|
||||
"delete_attachment_confirm": "",
|
||||
"delete_item_confirm": "",
|
||||
"delete_attachment_confirm": "Er du sikker på, at du vil slette denne vedhæftede fil?",
|
||||
"delete_item_confirm": "Er du sikker på, at du vil slette dette element?",
|
||||
"description": "Beskrivelse",
|
||||
"details": "Detaljer",
|
||||
"drag_and_drop": "Træk og slip filer her, eller klik for at vælge filer",
|
||||
"edit": {
|
||||
"edit_attachment_dialog": {
|
||||
"attachment_title": "",
|
||||
"attachment_type": "",
|
||||
"primary_photo": "",
|
||||
"primary_photo_sub": "",
|
||||
"select_type": "",
|
||||
"title": ""
|
||||
"attachment_title": "Titel på vedhæftet fil",
|
||||
"attachment_type": "Vedhæftningstype",
|
||||
"primary_photo": "Primært foto",
|
||||
"primary_photo_sub": "Denne mulighed er kun tilgængelig for fotos. Kun ét foto kan være primært. Hvis du vælger denne mulighed, vil det aktuelle primære foto, hvis der er et, blive fravalgt.",
|
||||
"select_type": "Vælg en type",
|
||||
"title": "Rediger vedhæftet fil"
|
||||
}
|
||||
},
|
||||
"edit_details": "Rediger detaljer",
|
||||
@@ -285,7 +305,7 @@
|
||||
"first": "Første",
|
||||
"include_archive": "Medtag arkiverede elementer",
|
||||
"insured": "Forsikret",
|
||||
"invalid_asset_id": "",
|
||||
"invalid_asset_id": "Ugyldigt aktiv-ID",
|
||||
"last": "Sidst",
|
||||
"lifetime_warranty": "livstidsgaranti",
|
||||
"location": "Lokalitet",
|
||||
@@ -296,7 +316,7 @@
|
||||
"name": "Navn",
|
||||
"negate_labels": "Ophæv valgte etiketter",
|
||||
"next_page": "Næste side",
|
||||
"no_attachments": "",
|
||||
"no_attachments": "Ingen vedhæftede filer fundet",
|
||||
"no_results": "Ingen elementer fundet",
|
||||
"notes": "Noter",
|
||||
"only_with_photo": "Kun elementer med foto",
|
||||
@@ -318,44 +338,44 @@
|
||||
"receipts": "Kvitteringer",
|
||||
"reset_search": "Nulstil Søgning",
|
||||
"results": "{ total } Wyniki",
|
||||
"select_field": "",
|
||||
"select_field": "Vælg et felt",
|
||||
"serial_number": "Serienummer",
|
||||
"show_advanced_view_options": "vis avancerede indstillinger",
|
||||
"sold_at": "Solgt D.",
|
||||
"sold_details": "Salgs detaljer",
|
||||
"sold_price": "Solgt pris",
|
||||
"sold_to": "Sold til",
|
||||
"sync_child_locations": "",
|
||||
"sync_child_locations": "Synkroniser placeringer af underordnede elementer",
|
||||
"tip_1": "Placerings- og etiketfiltre bruger betjeningen 'ELLER'. Hvis mere end én er valgt, kræves der kun én\n til et match.",
|
||||
"tip_2": "Søgninger med præfikset '#'' vil forespørge efter et aktiv-id (eksempel '#000-001')",
|
||||
"tip_3": "Feltfiltre bruger handlingen 'ELLER'. Hvis mere end én er valgt, kræves der kun én til en\n kamp.",
|
||||
"tips": "Tips",
|
||||
"tips_sub": "Søgetips",
|
||||
"toast": {
|
||||
"asset_not_found": "",
|
||||
"attachment_deleted": "",
|
||||
"attachment_updated": "",
|
||||
"attachment_uploaded": "",
|
||||
"child_items_location_no_longer_synced": "",
|
||||
"child_items_location_synced": "",
|
||||
"child_location_desync": "",
|
||||
"error_loading_parent_data": "",
|
||||
"failed_adjust_quantity": "",
|
||||
"failed_delete_attachment": "",
|
||||
"failed_delete_item": "",
|
||||
"failed_duplicate_item": "",
|
||||
"failed_load_asset": "",
|
||||
"failed_load_item": "",
|
||||
"failed_load_items": "",
|
||||
"failed_save": "",
|
||||
"failed_save_no_location": "",
|
||||
"failed_search_items": "",
|
||||
"failed_update_attachment": "",
|
||||
"failed_upload_attachment": "",
|
||||
"item_deleted": "",
|
||||
"item_saved": "",
|
||||
"quantity_cannot_negative": "",
|
||||
"sync_child_location": ""
|
||||
"asset_not_found": "Aktivet blev ikke fundet",
|
||||
"attachment_deleted": "Vedhæftet fil slettet",
|
||||
"attachment_updated": "Vedhæftet fil opdateret",
|
||||
"attachment_uploaded": "Vedhæftet fil uploadet",
|
||||
"child_items_location_no_longer_synced": "Placeringen af underelementer vil ikke længere blive synkroniseret med dette element.",
|
||||
"child_items_location_synced": "Placeringerne af underordnede elementer er blevet synkroniseret med dette element",
|
||||
"child_location_desync": "Ændring af placering vil afsynkronisere den fra forælderens placering",
|
||||
"error_loading_parent_data": "Noget gik galt under indlæsning af overordnede data",
|
||||
"failed_adjust_quantity": "Kunne ikke justere mængden",
|
||||
"failed_delete_attachment": "Kunne ikke slette vedhæftet fil",
|
||||
"failed_delete_item": "Kunne ikke slette elementet",
|
||||
"failed_duplicate_item": "Kunne ikke duplikere elementet",
|
||||
"failed_load_asset": "Kunne ikke indlæse aktiv",
|
||||
"failed_load_item": "Kunne ikke indlæse elementet",
|
||||
"failed_load_items": "Kunne ikke indlæse elementer",
|
||||
"failed_save": "Kunne ikke gemme elementet",
|
||||
"failed_save_no_location": "Kunne ikke gemme elementet: ingen placering valgt",
|
||||
"failed_search_items": "Kunne ikke søge efter elementer",
|
||||
"failed_update_attachment": "Kunne ikke opdatere vedhæftet fil",
|
||||
"failed_upload_attachment": "Kunne ikke uploade vedhæftet fil",
|
||||
"item_deleted": "Element slettet",
|
||||
"item_saved": "Element gemt",
|
||||
"quantity_cannot_negative": "Mængden må ikke være negativ",
|
||||
"sync_child_location": "Den valgte forælder synkroniserer sine børns placeringer med sine egne. Placeringen er blevet opdateret."
|
||||
},
|
||||
"updated_at": "Opdateret d.",
|
||||
"warranty": "Garanti",
|
||||
@@ -363,32 +383,32 @@
|
||||
"warranty_expires": "Garantien udløber"
|
||||
},
|
||||
"labels": {
|
||||
"label_delete_confirm": "",
|
||||
"label_delete_confirm": "Er du sikker på, at du vil slette denne etiket? Denne handling kan ikke fortrydes.",
|
||||
"no_results": "Ingen etiketter fundet",
|
||||
"toast": {
|
||||
"failed_delete_label": "",
|
||||
"failed_load_label": "",
|
||||
"failed_update_label": "",
|
||||
"label_deleted": "",
|
||||
"label_updated": ""
|
||||
"failed_delete_label": "Etiketten kunne ikke slettes",
|
||||
"failed_load_label": "Etiketten kunne ikke indlæses",
|
||||
"failed_update_label": "Etiketten kunne ikke opdateres",
|
||||
"label_deleted": "Etiket slettet",
|
||||
"label_updated": "Etiket opdateret"
|
||||
},
|
||||
"update_label": "Opdater etiket"
|
||||
},
|
||||
"languages": {
|
||||
"ca": "Catalansk",
|
||||
"cs-CZ": "",
|
||||
"cs-CZ": "Tjekkisk",
|
||||
"de": "Tysk",
|
||||
"en": "Engelsk",
|
||||
"es": "Spansk",
|
||||
"fi-FI": "",
|
||||
"fi-FI": "Finsk",
|
||||
"fr": "Fransk",
|
||||
"hu": "Ungarsk",
|
||||
"id-ID": "",
|
||||
"id-ID": "Indonesisk",
|
||||
"it": "Italiensk",
|
||||
"ja-JP": "Japansk",
|
||||
"ko-KR": "",
|
||||
"lb-LU": "",
|
||||
"lt-LT": "",
|
||||
"ko-KR": "Koreansk",
|
||||
"lb-LU": "Luxembourgsk (Luxembourg)",
|
||||
"lt-LT": "Litauisk (Litauen)",
|
||||
"nb-NO": "Norsk",
|
||||
"nl": "Hollandsk",
|
||||
"pl": "Polsk",
|
||||
@@ -396,7 +416,7 @@
|
||||
"pt-PT": "Portugisisk (Portugal)",
|
||||
"ru": "Russisk",
|
||||
"sl": "Slovensk",
|
||||
"sq-AL": "",
|
||||
"sq-AL": "Albansk",
|
||||
"sv": "Svensk",
|
||||
"ta-IN": "Tamilsk",
|
||||
"th-TH": "Thailandsk",
|
||||
@@ -413,15 +433,15 @@
|
||||
"locations": {
|
||||
"child_locations": "Underordnede placeringer",
|
||||
"collapse_tree": "Kollaps træ",
|
||||
"expand_tree": "",
|
||||
"location_items_delete_confirm": "",
|
||||
"expand_tree": "Udvid træ",
|
||||
"location_items_delete_confirm": "Er du sikker på, at du vil slette denne placering og alle dens elementer? Denne handling kan ikke fortrydes.",
|
||||
"no_results": "Ingen placeringer fundet",
|
||||
"toast": {
|
||||
"failed_delete_location": "",
|
||||
"failed_load_location": "",
|
||||
"failed_update_location": "",
|
||||
"location_deleted": "",
|
||||
"location_updated": ""
|
||||
"failed_delete_location": "Kunne ikke slette placeringen",
|
||||
"failed_load_location": "Placeringen kunne ikke indlæses",
|
||||
"failed_update_location": "Kunne ikke opdatere placeringen",
|
||||
"location_deleted": "Placering slettet",
|
||||
"location_updated": "Placering opdateret"
|
||||
},
|
||||
"update_location": "Opdatér sted"
|
||||
},
|
||||
@@ -478,10 +498,10 @@
|
||||
"currency_format": "Valuta format",
|
||||
"current_password": "Aktuel adgangskode",
|
||||
"delete_account": "Slet Konto",
|
||||
"delete_account_confirm": "",
|
||||
"delete_account_confirm": "Er du sikker på, at du vil slette din konto? Hvis du er det sidste medlem i din gruppe, vil alle dine data blive slettet. Denne handling kan ikke fortrydes.",
|
||||
"delete_account_sub": "Slet din konto og alle dens tilknyttede data. Dette kan ikke laves om.",
|
||||
"delete_notifier_confirm": "",
|
||||
"display_legacy_header": "",
|
||||
"delete_notifier_confirm": "Er du sikker på, at du vil slette denne underretter?",
|
||||
"display_legacy_header": "{ currentValue, select, true {Deaktiver Legacy Header} false {Aktiver Legacy Header} other {Ikke ramt}}",
|
||||
"enabled": "Aktiveret",
|
||||
"example": "Eksempel",
|
||||
"gen_invite": "Generer invitationslink",
|
||||
@@ -491,27 +511,27 @@
|
||||
"language": "Sprog",
|
||||
"new_password": "Ny Adgangskode",
|
||||
"no_notifiers": "Ingen notifikationer konfiguret",
|
||||
"no_override": "",
|
||||
"no_override": "Ingen tilsidesættelse",
|
||||
"notifier_modal": "{ type, select, true {Rediger} false {Opret} other {Andet}} Meddeler",
|
||||
"notifiers": "Meddelere",
|
||||
"notifiers_sub": "Få notifikationer om kommende vedligeholdelsespåmindelser",
|
||||
"override_locale": "",
|
||||
"override_locale": "Tilsidesæt dato og valutasprog",
|
||||
"test": "Test",
|
||||
"theme_settings": "Temaindstillinger",
|
||||
"theme_settings_sub": "Temaindstillinger gemmes i din browsers lokale lager. Du kan til enhver tid ændre temaet. Hvis du har\n problemer med at indstille dit tema, kan du prøve at opdatere din browser.",
|
||||
"toast": {
|
||||
"account_deleted": "",
|
||||
"failed_change_password": "",
|
||||
"failed_create_notifier": "",
|
||||
"failed_delete_account": "",
|
||||
"failed_delete_notifier": "",
|
||||
"failed_get_currencies": "",
|
||||
"failed_test_notifier": "",
|
||||
"failed_update_group": "",
|
||||
"failed_update_notifier": "",
|
||||
"group_updated": "",
|
||||
"notifier_test_success": "",
|
||||
"password_changed": ""
|
||||
"account_deleted": "Din konto er blevet slettet.",
|
||||
"failed_change_password": "Kunne ikke ændre adgangskode.",
|
||||
"failed_create_notifier": "Kunne ikke oprette underretteren.",
|
||||
"failed_delete_account": "Kunne ikke slette din konto.",
|
||||
"failed_delete_notifier": "Kunne ikke slette underretteren.",
|
||||
"failed_get_currencies": "Kunne ikke hente valutaer",
|
||||
"failed_test_notifier": "Kunne ikke teste underretteren.",
|
||||
"failed_update_group": "Gruppen kunne ikke opdateres",
|
||||
"failed_update_notifier": "Kunne ikke opdatere underretteren.",
|
||||
"group_updated": "Gruppen er opdateret",
|
||||
"notifier_test_success": "Underretter-testen er gennemført.",
|
||||
"password_changed": "Adgangskoden er ændret."
|
||||
},
|
||||
"update_group": "Opdatér Gruppe",
|
||||
"update_language": "Opdater sprogfil",
|
||||
@@ -521,61 +541,67 @@
|
||||
},
|
||||
"reports": {
|
||||
"label_generator": {
|
||||
"asset_end": "",
|
||||
"asset_start": "",
|
||||
"base_url": "",
|
||||
"bordered_labels": "",
|
||||
"generate_page": "",
|
||||
"input_placeholder": "",
|
||||
"instruction_1": "",
|
||||
"instruction_2": "",
|
||||
"instruction_3": "",
|
||||
"label_height": "",
|
||||
"label_width": "",
|
||||
"measure_type": "",
|
||||
"page_bottom_padding": "",
|
||||
"page_height": "",
|
||||
"page_left_padding": "",
|
||||
"page_right_padding": "",
|
||||
"page_top_padding": "",
|
||||
"page_width": "",
|
||||
"qr_code_example": "",
|
||||
"tip_1": "",
|
||||
"tip_2": "",
|
||||
"tip_3": "",
|
||||
"tips": "",
|
||||
"title": "",
|
||||
"asset_end": "Aktivets slut",
|
||||
"asset_start": "Aktivets start",
|
||||
"base_url": "Base URL",
|
||||
"bordered_labels": "Etiketter med kant",
|
||||
"generate_page": "Generer side",
|
||||
"input_placeholder": "Skriv her",
|
||||
"instruction_1": "Homebox Label Generator er et værktøj, der hjælper dig med at udskrive etiketter til dit Homebox-lager. Disse er beregnet til\nat være etiketter, der kan udskrives på forhånd, så du kan udskrive mange etiketter og have dem klar til påsætning.",
|
||||
"instruction_2": "Disse etiketter fungerer derfor ved at udskrive en URL, QR-kode og AssetID-oplysninger på en etiket. Hvis du har deaktiveret\nAssetID'er i dine Homebox-indstillinger, kan du stadig bruge dette værktøj, men AssetID'erne vil ikke referere til nogen varer.",
|
||||
"instruction_3": "Denne funktion er i de tidlige udviklingsfaser og kan ændres i fremtidige udgivelser. Hvis du har feedback, bedes\ndu give den i '<a href=\"https://github.com/sysadminsmedia/homebox/discussions/53\">'GitHub-diskussionen'</a>'",
|
||||
"label_height": "Etikethøjde",
|
||||
"label_width": "Etiketbredde",
|
||||
"measure_type": "Målingstype",
|
||||
"page_bottom_padding": "Sidebundspolstring",
|
||||
"page_height": "Sidehøjde",
|
||||
"page_left_padding": "Venstre sidepolstring",
|
||||
"page_right_padding": "Højre sidepolstring",
|
||||
"page_top_padding": "Sidetoppolstring",
|
||||
"page_width": "Sidebredde",
|
||||
"qr_code_example": "Eksempel på QR-kode",
|
||||
"tip_1": "Standardindstillingerne her er konfigureret for\n'<a href=\"https://www.avery.com/templates/5260\">'Avery 5260 etiketark'</a>'. Hvis du bruger et andet ark,\nskal du justere indstillingerne, så de passer til dit ark.",
|
||||
"tip_2": "Hvis du tilpasser dit ark, er dimensionerne i tommer. Da jeg byggede 5260-arket, opdagede jeg, at de\ndimensioner, der blev brugt i deres skabelon, ikke matchede det, der var nødvendigt for at udskrive i felterne.\n'<b>'Vær forberedt på nogle forsøg og fejl.'</b>'",
|
||||
"tip_3": "Ved printning sørg for at:\n'<ol><li>'Indstil margenerne til 0 eller Ingen'</li><li>'Indstil skaleringen til 100%'</li><li>'Deaktiver dobbeltsidet udskrivning'</li><li>'Udskriv en testside, før du udskriver flere sider'</li></ol>'",
|
||||
"tips": "Tips",
|
||||
"title": "Etiketgenerator",
|
||||
"toast": {
|
||||
"page_too_small_card": ""
|
||||
"page_too_small_card": "Sidestørrelsen er for lille til kortstørrelsen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"barcode_detected_message": "produkt stregkode opdaget",
|
||||
"barcode_fetch_data": "Hent produktdata",
|
||||
"error": "Der skete en fejl under skanningen",
|
||||
"invalid_url": "Ugyldig stregkode-URL",
|
||||
"no_sources": "Ingen videokilder er tilgængelig",
|
||||
"permission_denied": "",
|
||||
"permission_denied": "Kameratilladelse nægtet, Tillad venligst adgang til kameraet i dine browserindstillinger",
|
||||
"select_video_source": "Vælg en videokilde",
|
||||
"title": "",
|
||||
"title": "Skanner",
|
||||
"unsupported": "Media Stream API understøttes ikke uden HTTPS"
|
||||
},
|
||||
"tools": {
|
||||
"actions": "Handlinger på lagerbeholdning",
|
||||
"actions_set": {
|
||||
"create_missing_thumbnails": "Opret manglende miniaturebilleder",
|
||||
"create_missing_thumbnails_button": "Opret miniaturebilleder",
|
||||
"create_missing_thumbnails_confirm": "Er du sikker på, at du vil oprette manglende miniaturebilleder? Dette kan tage et stykke tid og kan ikke sættes på pause.",
|
||||
"create_missing_thumbnails_sub": "Opretter miniaturebilleder for alle vedhæftede filer, der understøttes af den aktuelle konfiguration. Dette er nyttigt for vedhæftede filer, der blev uploadet før v0.20.0-udgivelsen af Homebox. Dette overskriver ikke eksisterende miniaturebilleder, men opretter kun nye for vedhæftede filer, der ikke har et miniaturebillede. Bemærk, at miniaturebillederne oprettes i baggrunden og kan tage et stykke tid at fuldføre.",
|
||||
"ensure_ids": "Sørg for aktiv-id'er",
|
||||
"ensure_ids_button": "Sørg for aktiv-id'er",
|
||||
"ensure_ids_confirm": "",
|
||||
"ensure_ids_confirm": "Er du sikker på, at du vil sikre dig, at alle aktiver har et ID? Dette kan tage et stykke tid og kan ikke fortrydes.",
|
||||
"ensure_ids_sub": "Sikrer, at alle varer på lageret har et gyldigt asset_id felt. Dette gøres ved at finde det højeste aktuelle aktiv_id felt i databasen og anvende den næste værdi på hvert element, der har et ikke sat aktiv_id felt. Dette gøres i rækkefølge efter feltet opret_den.",
|
||||
"ensure_import_refs": "Sørg for importreferencer",
|
||||
"ensure_import_refs_button": "Sørg for importreferencer",
|
||||
"ensure_import_refs_sub": "Sikrer, at alle varer på lageret har et gyldigt import_ref felt. Dette gøres ved tilfældigt at generere en streng på 8 tegn for hvert element, der har et uindstillet import_ref felt.",
|
||||
"set_primary_photo": "Indstil primært foto",
|
||||
"set_primary_photo_button": "Indstil primært foto",
|
||||
"set_primary_photo_confirm": "",
|
||||
"set_primary_photo_confirm": "Er du sikker på, at du vil indstille primære billeder? Dette kan tage et stykke tid og kan ikke fortrydes.",
|
||||
"set_primary_photo_sub": "I version v0.10.0 af Homebox blev det primære billedfelt tilføjet til vedhæftede filer af typen foto. Denne handling indstiller det primære billedfelt til det første billede i matrixen for vedhæftede filer i databasen, hvis det ikke allerede er angivet. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'Se GitHub PR #576'</a>'",
|
||||
"zero_datetimes": "Nul Vare Dato Tider",
|
||||
"zero_datetimes_button": "Nul Varedato Tider",
|
||||
"zero_datetimes_confirm": "",
|
||||
"zero_datetimes_confirm": "Er du sikker på, at du vil nulstille alle dato- og klokkeslætsværdier? Dette kan tage et stykke tid og kan ikke fortrydes.",
|
||||
"zero_datetimes_sub": "Nulstiller klokkeslætsværdien for alle dato- og klokkeslætsfelter i lageret til begyndelsen af datoen. Dette er for at rette en fejl, der blev introduceret tidligt i udviklingen af webstedet, der forårsagede, at tidsværdien blev gemt med tiden, hvilket forårsagede problemer med datofelter, der viste nøjagtige værdier. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'Se Github-udgave #236 for flere detaljer.'</a>'"
|
||||
},
|
||||
"actions_sub": "Anvend flere handlinger på din beholdning på én gang. Det er uigenkaldelige handlinger. '<b>'Vær forsigtig.'</b>'",
|
||||
@@ -586,7 +612,7 @@
|
||||
"export_sub": "Eksporterer standard CSV-formatet til Homebox. Dette vil eksportere alle varer i dit lager.",
|
||||
"import": "Importeret beholdning",
|
||||
"import_button": "Importer beholdning",
|
||||
"import_ref_confirm": "",
|
||||
"import_ref_confirm": "Er du sikker på, at du vil sikre dig, at alle aktiver har en import_ref? Dette kan tage et stykke tid og kan ikke fortrydes.",
|
||||
"import_sub": "Importerer standard CSV-formatet til Homebox. Uden en '<code>'HB.import_ref'</code>'-kolonne vil dette '<b>'ikke'</b>' overskrive eksisterende genstande i dit lager, kun tilføje nye genstande. Rækker med kolonnen \"<code>HB.import_ref\"</code> flettes ind i eksisterende elementer med samme import_ref, hvis der findes en."
|
||||
},
|
||||
"import_export_sub": "Importér og eksporter din lagerbeholdning til og fra en CSV-fil. Dette er nyttigt til at migrere dit lager til en ny forekomst af Homebox.",
|
||||
@@ -601,11 +627,12 @@
|
||||
},
|
||||
"reports_sub": "Generer forskellige rapporter for dit lager.",
|
||||
"toast": {
|
||||
"asset_success": "",
|
||||
"failed_ensure_ids": "",
|
||||
"failed_ensure_import_refs": "",
|
||||
"failed_set_primary_photos": "",
|
||||
"failed_zero_datetimes": ""
|
||||
"asset_success": "Aktiverne i { results } er blevet opdateret.",
|
||||
"failed_create_missing_thumbnails": "Kunne ikke oprette manglende miniaturebilleder.",
|
||||
"failed_ensure_ids": "Kunne ikke sikre aktiv-ID'er.",
|
||||
"failed_ensure_import_refs": "Kunne ikke sikre importreferencer.",
|
||||
"failed_set_primary_photos": "Kunne ikke indstille primære billeder.",
|
||||
"failed_zero_datetimes": "Dato- og klokkeslætsværdier kunne ikke nulstilles."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"components": {
|
||||
"app": {
|
||||
"create_modal": {
|
||||
"createAndAddAnother": "Verwenden Sie {Umschalttaste} + {Eingabetaste}, um eine weitere zu erstellen und hinzuzufügen.",
|
||||
"createAndAddAnother": "Verwenden Sie {shiftKey} + {enterKey}, um eine weitere zu erstellen und hinzuzufügen.",
|
||||
"enter": "Eingabe",
|
||||
"shift": "Shift"
|
||||
},
|
||||
@@ -24,6 +24,13 @@
|
||||
"new_version_available_link": "Klicken Sie hier, um die Release Notes anzuzeigen"
|
||||
}
|
||||
},
|
||||
"color_selector": {
|
||||
"clear": "Farbe löschen",
|
||||
"color": "Farbe",
|
||||
"no_color": "Keine Farbe",
|
||||
"no_color_selected": "Keine Farbe ausgewählt",
|
||||
"randomize": "Zufällige Farbe"
|
||||
},
|
||||
"form": {
|
||||
"password": {
|
||||
"toggle_show": "Passwort anzeigen"
|
||||
@@ -93,6 +100,8 @@
|
||||
"item_photo": "Artikel Bild",
|
||||
"item_quantity": "Anzahl der Artikel",
|
||||
"parent_item": "Übergeordneter Gegenstand",
|
||||
"product_tooltip_input_barcode": "Automatisch ausfüllen mit einem manuell bereitgestellten Barcode",
|
||||
"product_tooltip_scan_barcode": "Automatisch ausfüllen mit einem Barcode von 📷",
|
||||
"rotate_photo": "Photo drehen",
|
||||
"set_as_primary_photo": "Festlegen als { isPrimary, select, true {non-} false {} other {}}primäres Foto",
|
||||
"title": "Gegenstand erstellen",
|
||||
@@ -105,18 +114,27 @@
|
||||
"please_select_location": "Bitte einen Ort auswählen.",
|
||||
"rotate_failed": "Drehen des Bildes fehlgeschlagen: {error}",
|
||||
"rotate_process_failed": "Das gedrehte Bild konnte nicht verarbeitet werden",
|
||||
"some_photos_failed": "{Anzahl, Plural, =0 {Keine Fotos zum Hochladen.} =1 {1 Foto konnte nicht hochgeladen werden.} andere {Einige Fotos konnten nicht hochgeladen werden.}}",
|
||||
"some_photos_failed": "{count, plural, =0 {Keine Fotos zum Hochladen.} =1 {1 Foto konnte nicht hochgeladen werden.} other {Einige Fotos konnten nicht hochgeladen werden.}}",
|
||||
"upload_failed": "Hochladen des Bildes Fehlgeschlagen: { photoName }",
|
||||
"upload_success": "{Anzahl, Plural, =0 {Keine Fotos hochgeladen.} =1 {Foto erfolgreich hochgeladen.} andere {Alle Fotos erfolgreich hochgeladen.}}",
|
||||
"uploading_photos": "{Anzahl, Plural, =0 {Keine Fotos zum Hochladen} =1 {1 Foto wird hochgeladen...} andere {{Anzahl} Fotos werden hochgeladen...}}"
|
||||
"upload_success": "{count, plural, =0 {Keine Fotos hochgeladen.} =1 {Foto erfolgreich hochgeladen.} other {Alle Fotos erfolgreich hochgeladen.}}",
|
||||
"uploading_photos": "{count, plural, =0 {Keine Fotos zum Hochladen} =1 {1 Foto wird hochgeladen...} other {{count} Fotos werden hochgeladen...}}"
|
||||
},
|
||||
"upload_photos": "Upload Bilder",
|
||||
"uploaded": "Bild hochgeladen"
|
||||
},
|
||||
"product_import": {
|
||||
"barcode": "Produkt-Strichcode",
|
||||
"db_source": "DB-Quelle",
|
||||
"error_exception": "Beim Abrufen des Artikel-Barcodes ist ein Problem aufgetreten: ",
|
||||
"error_invalid_barcode": "Ungültiger Barcode angegeben",
|
||||
"error_not_found": "Kein Produkt mit dem angegebenen Barcode gefunden.",
|
||||
"search_item": "Produkt suchen",
|
||||
"title": "Produkt importieren"
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "Keine Ergebnisse gefunden",
|
||||
"placeholder": "Auswählen...",
|
||||
"search_placeholder": "Für Suche tippen..."
|
||||
"placeholder": "Auswählen…",
|
||||
"search_placeholder": "Für Suche tippen…"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
@@ -136,6 +154,7 @@
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"label_color": "Label-Farbe",
|
||||
"label_description": "Label-Beschreibung",
|
||||
"label_name": "Label-Name",
|
||||
"title": "Label erstellen",
|
||||
@@ -168,7 +187,7 @@
|
||||
"select_location": "Standort wählen"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "Keine Standorte verfügbar. Fügen Sie neue Standorte über die Schaltfläche\n `<`span class=\"link-primary\"`>`Erstellen`<`/span`>` in der Navigationsleiste hinzu."
|
||||
"no_locations": "Keine Standorte verfügbar. Fügen Sie neue Standorte über die Schaltfläche\n `<span class=\"link-primary\">`Erstellen`</span>` in der Navigationsleiste hinzu."
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
@@ -176,6 +195,9 @@
|
||||
"shortcut_hint": "Verwenden Sie die Zifferntasten, um schnell eine Aktion auszuwählen."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"api_failure": "Backend-API-Aufruf fehlgeschlagen: "
|
||||
},
|
||||
"global": {
|
||||
"add": "Hinzufügen",
|
||||
"archived": "Archiviert",
|
||||
@@ -196,14 +218,14 @@
|
||||
"follow_dev": "Dem Entwickler folgen",
|
||||
"footer": {
|
||||
"api_link": "'<a href=\"https://homebox.software/en/api/\" target=\"_blank\">'API'</a>'",
|
||||
"version_link": "'<a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/'{ version }\" target=\"_blank\"> Version: { version } Erstellt: { build } '</a>'"
|
||||
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Version: { version } Erstellt: { build } '</a>'"
|
||||
},
|
||||
"github": "GitHub-Projekt",
|
||||
"insured": "Versichert",
|
||||
"items": "Gegenstände",
|
||||
"join_discord": "Discord beitreten",
|
||||
"labels": "Labels",
|
||||
"loading": "Wird geladen …",
|
||||
"loading": "Wird geladen…",
|
||||
"locations": "Lagerorte",
|
||||
"maintenance": "Wartung",
|
||||
"name": "Name",
|
||||
@@ -375,6 +397,7 @@
|
||||
"update_label": "Label aktualisieren"
|
||||
},
|
||||
"languages": {
|
||||
"bs-BA": "Bosnisch (Bosnien und Herzegowina)",
|
||||
"ca": "Katalanisch",
|
||||
"cs-CZ": "Tschechisch",
|
||||
"de": "Deutsch",
|
||||
@@ -401,9 +424,10 @@
|
||||
"th-TH": "Thailändisch",
|
||||
"tr": "Türkisch",
|
||||
"uk-UA": "Ukrainisch",
|
||||
"zh-CN": "Chinesisch (einfach)",
|
||||
"vi-VN": "Vietnamesisch",
|
||||
"zh-CN": "Chinesisch (vereinfacht)",
|
||||
"zh-HK": "Chinesisch (Hong Kong)",
|
||||
"zh-MO": "Chinesisch (Macao)",
|
||||
"zh-MO": "Chinesisch (Macau)",
|
||||
"zh-TW": "Chinesisch (traditionell)"
|
||||
},
|
||||
"languages.da-DK": "Dänisch",
|
||||
@@ -415,7 +439,7 @@
|
||||
"collapse_tree": "Baum einklappen",
|
||||
"expand_tree": "Baum ausklappen",
|
||||
"location_items_delete_confirm": "Möchten Sie diesen Standort und alle darin enthaltenen Elemente wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"no_results": "Keine Orte gefunden",
|
||||
"no_results": "Keine Standorte gefunden",
|
||||
"toast": {
|
||||
"failed_delete_location": "Standort konnte nicht gelöscht werden",
|
||||
"failed_load_location": "Standort konnte nicht geladen werden",
|
||||
@@ -541,7 +565,7 @@
|
||||
"page_width": "Seitenbreite",
|
||||
"qr_code_example": "QR-Code Beispiel",
|
||||
"tip_1": "Die Standardeinstellungen hier sind für die\n'<a href=\"https://www.avery.com/templates/5260\">'Avery 5260 Etikettenbögen'</a>'. Wenn Sie einen anderen Bogen verwenden,\nmüssen Sie die Einstellungen an Ihr Blatt anpassen.",
|
||||
"tip_2": "Wenn Sie Ihr Blatt anpassen, werden die Abmessungen in Zoll angegeben. Beim Erstellen des 5260-Blattes habe ich festgestellt, dass die\nin deren Vorlage verwendeten Abmessungen nicht mit den für den Druck in den Feldern erforderlichen Abmessungen übereinstimmen.\n„<b>Seien Sie auf einige Versuche und Irrtümer gefasst.“</b>",
|
||||
"tip_2": "Wenn Sie Ihr Blatt anpassen, werden die Abmessungen in Zoll angegeben. Beim Erstellen des 5260-Blattes habe ich festgestellt, dass die\nin deren Vorlage verwendeten Abmessungen nicht mit den für den Druck in den Feldern erforderlichen Abmessungen übereinstimmen.\n'<b>'Seien Sie auf einige Versuche und Irrtümer gefasst.'</b>'",
|
||||
"tip_3": "Achten Sie beim Drucken auf Folgendes:\n'<ol><li>'Setzen Sie die Ränder auf 0 oder Keine'</li><li>'Setzen Sie die Skalierung auf 100 %'</li><li>'Deaktivieren Sie den beidseitigen Druck'</li><li>'Drucken Sie eine Testseite, bevor Sie mehrere Seiten drucken'</li></ol>'",
|
||||
"tips": "Tipps",
|
||||
"title": "Etikettengenerator",
|
||||
@@ -551,6 +575,8 @@
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"barcode_detected_message": "Produkt-Barcode erkannt",
|
||||
"barcode_fetch_data": "Produktdaten abrufen",
|
||||
"error": "Beim Scannen ist ein Fehler aufgetreten",
|
||||
"invalid_url": "Ungültige Barcode-URL",
|
||||
"no_sources": "Keine Videoquellen verfügbar",
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
"item_photo": "Item Photo 📷",
|
||||
"item_quantity": "Item Quantity",
|
||||
"parent_item": "Parent Item",
|
||||
"product_tooltip_input_barcode": "Autofill with a manually provided barcode",
|
||||
"product_tooltip_scan_barcode": "Autofill with a barcode from 📷",
|
||||
"rotate_photo": "Rotate photo",
|
||||
"set_as_primary_photo": "Set as { isPrimary, select, true {non-} false {} other {}}primary photo",
|
||||
"title": "Create Item",
|
||||
@@ -115,15 +117,24 @@
|
||||
"some_photos_failed": "{count, plural, =0 {No photos to upload.} =1 {1 photo failed to upload.} other {Some photos failed to upload.}}",
|
||||
"upload_failed": "Failed to upload photo: { photoName }",
|
||||
"upload_success": "{count, plural, =0 {No photos uploaded.} =1 {Photo uploaded successfully.} other {All photos uploaded successfully.}}",
|
||||
"uploading_photos": "{count, plural, =0 {No photos to upload} =1 {Uploading 1 photo...} other {Uploading {count} photos...}}"
|
||||
"uploading_photos": "{count, plural, =0 {No photos to upload} =1 {Uploading 1 photo…} other {Uploading {count} photos…}}"
|
||||
},
|
||||
"upload_photos": "Upload Photos",
|
||||
"uploaded": "Uploaded Photo"
|
||||
},
|
||||
"product_import": {
|
||||
"barcode": "Product's barcode",
|
||||
"db_source": "DB source",
|
||||
"error_exception": "Exception occured while retrieving item barcode: ",
|
||||
"error_invalid_barcode": "Invalid barcode provided",
|
||||
"error_not_found": "No product found with given barcode.",
|
||||
"search_item": "Search product",
|
||||
"title": "Import product"
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "No Results Found",
|
||||
"placeholder": "Select...",
|
||||
"search_placeholder": "Type to search..."
|
||||
"placeholder": "Select…",
|
||||
"search_placeholder": "Type to search…"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
@@ -176,7 +187,7 @@
|
||||
"select_location": "Select a Location"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "No locations available. Add new locations through the\n `<`span class=\"link-primary\"`>`Create`<`/span`>` button on the navigation bar."
|
||||
"no_locations": "No locations available. Add new locations through the\n '<span class=\"link-primary\">'Create'</span>' button on the navigation bar."
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
@@ -184,6 +195,9 @@
|
||||
"shortcut_hint": "Use the number keys to quickly select an action."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"api_failure": "Backend API call failed: "
|
||||
},
|
||||
"global": {
|
||||
"add": "Add",
|
||||
"archived": "Archived",
|
||||
@@ -204,14 +218,14 @@
|
||||
"follow_dev": "Follow the Developer",
|
||||
"footer": {
|
||||
"api_link": "'<a href=\"https://homebox.software/en/api/\" target=\"_blank\">'API'</a>'",
|
||||
"version_link": "'<a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/v'{ version }\" target=\"_blank\"> Version: { version } Build: { build } '</a>'"
|
||||
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Version: { version } Build: { build } '</a>'"
|
||||
},
|
||||
"github": "GitHub Project",
|
||||
"insured": "Insured",
|
||||
"items": "Items",
|
||||
"join_discord": "Join the Discord",
|
||||
"labels": "Labels",
|
||||
"loading": "Loading...",
|
||||
"loading": "Loading…",
|
||||
"locations": "Locations",
|
||||
"maintenance": "Maintenance",
|
||||
"name": "Name",
|
||||
@@ -275,6 +289,18 @@
|
||||
"delete_attachment_confirm": "Are you sure you want to delete this attachment?",
|
||||
"delete_item_confirm": "Are you sure you want to delete this item?",
|
||||
"description": "Description",
|
||||
"duplicate": {
|
||||
"prefix": "Copy of ",
|
||||
"copy_maintenance": "Copy Maintenance",
|
||||
"copy_attachments": "Copy Attachments",
|
||||
"copy_custom_fields": "Copy Custom Fields",
|
||||
"custom_prefix": "Copy Prefix",
|
||||
"enable_custom_prefix": "Enable Custom Prefix",
|
||||
"prefix_instructions": "This prefix will be added to the beginning of the duplicated item's name. Include a space at the end of the prefix to add a space between the prefix and the item name.",
|
||||
"temporary_title": "Temporary Settings",
|
||||
"title": "Duplicate Settings",
|
||||
"override_instructions": "Hold shift when clicking the duplicate button to override these settings."
|
||||
},
|
||||
"details": "Details",
|
||||
"drag_and_drop": "Drag and drop files here or click to select files",
|
||||
"edit": {
|
||||
@@ -285,7 +311,8 @@
|
||||
"primary_photo_sub": "This option is only available for photos. Only one photo can be primary. If you select this option, the current primary photo, if any will be unselected.",
|
||||
"select_type": "Select a type",
|
||||
"title": "Attachment Edit"
|
||||
}
|
||||
},
|
||||
"view_image": "View Image"
|
||||
},
|
||||
"edit_details": "Edit Details",
|
||||
"field_selector": "Field Selector",
|
||||
@@ -383,6 +410,7 @@
|
||||
"update_label": "Update Label"
|
||||
},
|
||||
"languages": {
|
||||
"bs-BA": "Bosnian (Bosnia and Herzegovina)",
|
||||
"ca": "Catalan",
|
||||
"cs-CZ": "Czech",
|
||||
"da-DK": "Danish",
|
||||
@@ -413,6 +441,7 @@
|
||||
"th-TH": "Thai",
|
||||
"tr": "Turkish",
|
||||
"uk-UA": "Ukrainian",
|
||||
"vi-VN": "Vietnamese",
|
||||
"zh-CN": "Chinese (Simplified)",
|
||||
"zh-HK": "Chinese (Hong Kong)",
|
||||
"zh-MO": "Chinese (Macau)",
|
||||
@@ -559,6 +588,8 @@
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"barcode_detected_message": "product barcode detected",
|
||||
"barcode_fetch_data": "Fetch product data",
|
||||
"error": "An error occurred while scanning",
|
||||
"invalid_url": "Invalid barcode URL",
|
||||
"no_sources": "No video sources available",
|
||||
|
||||
@@ -24,6 +24,13 @@
|
||||
"new_version_available_link": "Haz click aquí para ver las notas de la versión"
|
||||
}
|
||||
},
|
||||
"color_selector": {
|
||||
"clear": "Borrar color",
|
||||
"color": "Color",
|
||||
"no_color": "Sin color",
|
||||
"no_color_selected": "Ningún color seleccionado",
|
||||
"randomize": "Aleatorizar color"
|
||||
},
|
||||
"form": {
|
||||
"password": {
|
||||
"toggle_show": "Alternar Visibilidad de la Contraseña"
|
||||
@@ -93,6 +100,8 @@
|
||||
"item_photo": "Foto del artículo 📷",
|
||||
"item_quantity": "Cantidad de Elementos",
|
||||
"parent_item": "Elemento Padre",
|
||||
"product_tooltip_input_barcode": "Autocompletar con un código de barras proporcionado manualmente",
|
||||
"product_tooltip_scan_barcode": "Autocompletar con un código de barras desde 📷",
|
||||
"rotate_photo": "Girar foto",
|
||||
"set_as_primary_photo": "Establecer como { isPrimary, select, true {non-} false {} other {}} foto principal",
|
||||
"title": "Crear Elemento",
|
||||
@@ -108,15 +117,24 @@
|
||||
"some_photos_failed": "{count, plural, =0 {No hay fotos para subir.} =1 {1 foto no se ha podido subir.} other {Algunas fotos no se han podido subir.}}",
|
||||
"upload_failed": "Error al subir la foto: { photoName }",
|
||||
"upload_success": "{count, plural, =0 {No hay fotos subidas.} =1 {Foto subida con éxito.} other {Todas las fotos subidas con éxito.}}",
|
||||
"uploading_photos": "{count, plural, =0 {No hay fotos para subir} =1 {Subiendo 1 foto...} other {Subiendo {count} fotos...}}"
|
||||
"uploading_photos": "{count, plural, =0 {No hay fotos para subir} =1 {Subiendo 1 foto…} other {Subiendo {count} fotos…}}"
|
||||
},
|
||||
"upload_photos": "Fotos Subidas",
|
||||
"uploaded": "Foto Subida"
|
||||
},
|
||||
"product_import": {
|
||||
"barcode": "Código de barras del producto",
|
||||
"db_source": "Fuente de la base de datos",
|
||||
"error_exception": "Se ha producido una excepción al recuperar el código de barras del artículo: ",
|
||||
"error_invalid_barcode": "Código de barras proporcionado no válido",
|
||||
"error_not_found": "No se ha encontrado ningún producto con código de barras.",
|
||||
"search_item": "Buscar producto",
|
||||
"title": "Importar producto"
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "Resultados No Encontrados",
|
||||
"placeholder": "Seleccionar...",
|
||||
"search_placeholder": "Escribe para buscar..."
|
||||
"placeholder": "Seleccionar…",
|
||||
"search_placeholder": "Escribe para buscar…"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
@@ -136,6 +154,7 @@
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"label_color": "Color de Etiqueta",
|
||||
"label_description": "Descripción de la etiqueta",
|
||||
"label_name": "Nombre de la Etiqueta",
|
||||
"title": "Crear Etiqueta",
|
||||
@@ -168,7 +187,7 @@
|
||||
"select_location": "Elegir una Ubicación"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "No hay ubicaciones disponibles. Añade nuevas ubicaciones mediante el botón de\n`<`span class=\"link-primary\"`>`Crear`<`/span`>` en la barra de navegación."
|
||||
"no_locations": "No hay ubicaciones disponibles. Añade nuevas ubicaciones mediante el botón de\n'<span class=\"link-primary\">'Crear'</span>' en la barra de navegación."
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
@@ -176,6 +195,9 @@
|
||||
"shortcut_hint": "Usa las teclas numéricas para seleccionar rápidamente una acción."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"api_failure": "Error en la llamada a la API del backend: "
|
||||
},
|
||||
"global": {
|
||||
"add": "Añadir",
|
||||
"archived": "Archivado",
|
||||
@@ -196,14 +218,14 @@
|
||||
"follow_dev": "Seguir al Desarrollador",
|
||||
"footer": {
|
||||
"api_link": "'<a href=\"https://homebox.software/en/api/\" target=\"_blank\">'API'</a>'",
|
||||
"version_link": "'<a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/'{ version }\" target=\"_blank\"> Versión: { version } Compilación: { build } '</a>'"
|
||||
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Versión: { version } Compilación: { build } '</a>'"
|
||||
},
|
||||
"github": "Proyecto GitHub",
|
||||
"insured": "Asegurado",
|
||||
"items": "Elementos",
|
||||
"join_discord": "Únete al Discord",
|
||||
"labels": "Etiquetas",
|
||||
"loading": "Cargando...",
|
||||
"loading": "Cargando…",
|
||||
"locations": "Ubicaciones",
|
||||
"maintenance": "Mantenimiento",
|
||||
"name": "Nombre",
|
||||
@@ -529,7 +551,7 @@
|
||||
"input_placeholder": "Escribe aquí",
|
||||
"instruction_1": "El Generador de Etiquetas Homebox es una herramienta que para ayudarte a imprimir etiquetas para tu inventario Homebox. Están pensadas para\n ser etiquetas de impresión anticipada para que puedas imprimir muchas etiquetas y tenerlas listas para usarlas",
|
||||
"instruction_2": "Como tal, estas etiquetas funcionan imprimiendo un código QR de URL e información ID de Activo en una etiqueta. Si has desactivadod\n ID de Activo en la configuración de tu Homebox, puedes seguir utilizando esta herramienta, pero los IDs de Activo no harán referencia a ningún elemento",
|
||||
"instruction_3": "Esta función se encuentra en las primeras etapas de desarrollo y puede cambiar en futuras versiones. Si tienes algún comentario, indícalo\n en la<a href=\"https://github.com/sysadminsmedia/homebox/discussions/53\"> «Discusión de GitHub»</a>",
|
||||
"instruction_3": "Esta función se encuentra en las primeras etapas de desarrollo y puede cambiar en futuras versiones. Si tienes algún comentario, indícalo\n en la '<a href=\"https://github.com/sysadminsmedia/homebox/discussions/53\">'Discusión de GitHub'</a>'",
|
||||
"label_height": "Altura de la Etiqueta",
|
||||
"label_width": "Ancho de la Etiqueta",
|
||||
"measure_type": "Tipo de Medida",
|
||||
@@ -551,6 +573,8 @@
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"barcode_detected_message": "código de barras del producto detectado",
|
||||
"barcode_fetch_data": "Obtener datos del producto",
|
||||
"error": "Se ha producido un error mientras se escaneaba",
|
||||
"invalid_url": "URL de código de barras inválido",
|
||||
"no_sources": "No hay fuentes de vídeo disponibles",
|
||||
@@ -562,6 +586,10 @@
|
||||
"tools": {
|
||||
"actions": "Acciones de Inventario",
|
||||
"actions_set": {
|
||||
"create_missing_thumbnails": "Crear Miniaturas que Faltan",
|
||||
"create_missing_thumbnails_button": "Crear Miniaturas",
|
||||
"create_missing_thumbnails_confirm": "¿Estás seguro de que deseas crear las miniaturas que faltan? Esto puede tardar un poco y no se puede pausar.",
|
||||
"create_missing_thumbnails_sub": "Crea miniaturas para todos los archivos adjuntos compatibles con la configuración actual. Esto es útil para los adjuntos que se subieron antes de la versión v0.20.0 de Homebox. Esto no sobreescribirá las miniaturas existentes, sólo creará nuevas para los adjuntos que no tienen una miniatura. Ten en cuenta que las miniaturas se crean en segundo plano y pueden tardar un poco en completarse.",
|
||||
"ensure_ids": "Asegurar IDs de Activos",
|
||||
"ensure_ids_button": "Asignar ID a los artículos",
|
||||
"ensure_ids_confirm": "¿Estás seguro de que quieres asegurarte de que todos los activos tengan un ID? Esto puede tardar un tiempo y no puede deshacerse.",
|
||||
@@ -573,12 +601,12 @@
|
||||
"set_primary_photo_button": "Establecer Foto Principal",
|
||||
"set_primary_photo_confirm": "¿Estás seguro de que quieres configurar las fotos principales? Esto puede tardar un tiempo y no se puede deshacer.",
|
||||
"set_primary_photo_sub": "En la versión v0.10.0 de Homebox, se añadió el indicador de imagen principal a los ficheros adjuntos de tipo foto. Esta acción establecerá la primera imagen de cada artículo como su imagen principal, si no hay una imagen principal ya definida. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'Ver PR #576 en GitHub'</a>'",
|
||||
"zero_datetimes": "Cero Horas Elementos",
|
||||
"zero_datetimes_button": "Cero Horas Elementos",
|
||||
"zero_datetimes": "Poner a cero las horas de los artículos",
|
||||
"zero_datetimes_button": "Poner a cero las horas de los artículos",
|
||||
"zero_datetimes_confirm": "¿Estás seguro de que deseas restablecer todos los valores de fecha y hora? Esto puede tardar un tiempo y no se puede deshacer.",
|
||||
"zero_datetimes_sub": "Restablece el valor de la hora para todos los campos de fecha/hora en tu inventario al principio de esa fecha. Esto se hace para corregir un error que se introdujo al principio del desarrollo de la aplicación, que causó que el valor de la hora se almacenase con la fecha, lo cual produjo problemas al mostrar valores precisos del campo. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'Ver el issue #236 de GitHub para más detalles.'</a>'"
|
||||
},
|
||||
"actions_sub": "Aplica Acciones a tu inventario de forma masiva. Estas son acciones irreversibles. '<b>'Ten Cuidado.'</b>'",
|
||||
"actions_sub": "Aplica Acciones a tu inventario de forma masiva. Estas acciones son irreversibles. '<b>'Ten Cuidado.'</b>'",
|
||||
"import_export": "Importar/Exportar",
|
||||
"import_export_set": {
|
||||
"export": "Exportar Inventario",
|
||||
@@ -586,7 +614,7 @@
|
||||
"export_sub": "Exporta el formato CSV estándar para Homebox. Esto exportará todos los elementos de tu inventario.",
|
||||
"import": "Importar Inventario",
|
||||
"import_button": "Importar Inventario",
|
||||
"import_ref_confirm": "¿Estás seguro de que deseas asegurarse de que todos los activos tengan un import_ref? Esto puede tardar un tiempo y no se puede deshacer.",
|
||||
"import_ref_confirm": "¿Estás seguro de que deseas asegurarte de que todos los activos tengan un import_ref? Esto puede tardar un tiempo y no se puede deshacer.",
|
||||
"import_sub": "Importa el formato CSV estándar para Homebox. Sin una columna '<code>'HB.import_ref'</code>', esto '<b>'no'</b>' sobrescribirá cualquier elemento existente en tu inventario, sólo añadirá nuevos artículos. Las filas con una columna '<code>'HB.import_ref'</code>' se fusionan con los artículos existentes con la misma import_ref, si existe."
|
||||
},
|
||||
"import_export_sub": "Importa y exporta tu inventario a y desde un archivo CSV. Esto es útil para migrar tu inventario a una nueva instancia de Homebox.",
|
||||
@@ -596,12 +624,13 @@
|
||||
"asset_labels_button": "Generador de Etiquetas",
|
||||
"asset_labels_sub": "Genera un PDF para impresión de etiquetas para un rango de IDs de Activos. Estas etiquetas no son específicas para tu inventario, por lo que puedes imprimirlas con antelación y aplicarlas a tu inventario cuando las recibas.",
|
||||
"bill_of_materials": "Lista de Materiales",
|
||||
"bill_of_materials_button": "Generar lista de materiales",
|
||||
"bill_of_materials_button": "Generar Lista de Materiales",
|
||||
"bill_of_materials_sub": "Genera un archivo CSV (Valores Separados por Comas) que puede importarse a un programa de hojas de cálculo. Es un resumen de tu inventario con información básica sobre artículos y precios."
|
||||
},
|
||||
"reports_sub": "Genera diferentes informes para tu inventario.",
|
||||
"toast": {
|
||||
"asset_success": "Se han actualizado { results } activos.",
|
||||
"failed_create_missing_thumbnails": "No se han podido crear las miniaturas que faltaban.",
|
||||
"failed_ensure_ids": "Error al asegurar los ID de los activos.",
|
||||
"failed_ensure_import_refs": "Error al asegurar las ref. de importación.",
|
||||
"failed_set_primary_photos": "No se han podido establecer las fotos principales.",
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"create_modal": {
|
||||
"createAndAddAnother": "",
|
||||
"enter": "",
|
||||
"shift": ""
|
||||
},
|
||||
"import_dialog": {
|
||||
"change_warning": "Olemassa olevien import_refs-tiedostojen tuonnin käyttäytyminen on muuttunut. Jos CSV-tiedostossa on import_ref, \nkohde päivitetään CSV-tiedoston arvoilla.",
|
||||
"description": "Tuo CSV-tiedosto, joka sisältää kohteesi, tarrasi ja sijaintisi. Katso lisätietoja dokumentaatiosta \nvaadittu muoto.",
|
||||
"title": "Tuo CSV-tiedosto",
|
||||
"toast": {
|
||||
"import_failed": "",
|
||||
"import_success": "",
|
||||
"please_select_file": ""
|
||||
}
|
||||
"title": "Tuo CSV-tiedosto"
|
||||
},
|
||||
"outdated": {
|
||||
"current_version": "Nykyinen versio",
|
||||
@@ -24,11 +14,6 @@
|
||||
"new_version_available_link": "Klikkaa tästä nähdäksesi julkaisutiedot"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"password": {
|
||||
"toggle_show": ""
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"copy_text": {
|
||||
"documentation": "dokumentointi",
|
||||
@@ -66,12 +51,7 @@
|
||||
"download": "Lataa Tarra",
|
||||
"print": "Tulosta tarra",
|
||||
"server_print": "Tulosta palvelimella",
|
||||
"titles": "Tarra",
|
||||
"toast": {
|
||||
"load_status_failed": "",
|
||||
"print_failed": "",
|
||||
"print_success": ""
|
||||
}
|
||||
"titles": "Tarra"
|
||||
},
|
||||
"page_qr_code": {
|
||||
"page_url": "Sivun URL",
|
||||
@@ -82,41 +62,13 @@
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"attachments_list": {
|
||||
"download": "",
|
||||
"open_new_tab": ""
|
||||
},
|
||||
"create_modal": {
|
||||
"delete_photo": "",
|
||||
"item_description": "Kohteen Kuvaus",
|
||||
"item_name": "Tuotteen Nimi",
|
||||
"item_photo": "Kohteen Kuva 📷",
|
||||
"item_quantity": "Tuotteen Määrä",
|
||||
"parent_item": "",
|
||||
"rotate_photo": "",
|
||||
"set_as_primary_photo": "",
|
||||
"title": "Luo Kohde",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": "",
|
||||
"failed_load_parent": "",
|
||||
"no_canvas_support": "",
|
||||
"please_select_location": "",
|
||||
"rotate_failed": "",
|
||||
"rotate_process_failed": "",
|
||||
"some_photos_failed": "",
|
||||
"upload_failed": "",
|
||||
"upload_success": "",
|
||||
"uploading_photos": ""
|
||||
},
|
||||
"upload_photos": "Lataa Valokuvia",
|
||||
"uploaded": ""
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "",
|
||||
"placeholder": "",
|
||||
"search_placeholder": ""
|
||||
"upload_photos": "Lataa Valokuvia"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
@@ -129,8 +81,7 @@
|
||||
"headers": "Otsikko",
|
||||
"page": "Sivu",
|
||||
"rows_per_page": "Rivejä sivua kohti",
|
||||
"table_settings": "Taulukon Asetukset",
|
||||
"view_item": ""
|
||||
"table_settings": "Taulukon Asetukset"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -138,13 +89,7 @@
|
||||
"create_modal": {
|
||||
"label_description": "Etiketin Kuvaus",
|
||||
"label_name": "Etiketin Nimi",
|
||||
"title": "Luo Tarra",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": "",
|
||||
"label_name_too_long": ""
|
||||
}
|
||||
"title": "Luo Tarra"
|
||||
},
|
||||
"selector": {
|
||||
"select_labels": "Valitse Tarrat"
|
||||
@@ -154,12 +99,7 @@
|
||||
"create_modal": {
|
||||
"location_description": "Sijainnin Kuvaus",
|
||||
"location_name": "Sijainnin Nimi",
|
||||
"title": "Luo Sijainti",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": ""
|
||||
}
|
||||
"title": "Luo Sijainti"
|
||||
},
|
||||
"selector": {
|
||||
"no_location_found": "Sijaintia ei löytynyt",
|
||||
@@ -184,11 +124,8 @@
|
||||
"confirm": "Vahvistaa",
|
||||
"create": "Luoda",
|
||||
"create_and_add": "Luo ja lisää toinen",
|
||||
"create_subitem": "",
|
||||
"created": "Luotu",
|
||||
"delete": "Poistaa",
|
||||
"delete_confirm": "",
|
||||
"demo_instance": "",
|
||||
"details": "Tiedot",
|
||||
"duplicate": "Kaksoiskappale",
|
||||
"edit": "Muokkaa",
|
||||
@@ -196,14 +133,13 @@
|
||||
"follow_dev": "Seuraa kehittäjää",
|
||||
"footer": {
|
||||
"api_link": "'<a href=\"https://homebox.software/en/api/\" target=\"_blank\">'API'</a>'",
|
||||
"version_link": "'<a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/'{ version }\" target=\"_blank\"> Versio: { version } Rakenna: { build } '</a>'"
|
||||
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Versio: { version } Rakenna: { build } '</a>'"
|
||||
},
|
||||
"github": "GitHub Projekti",
|
||||
"insured": "Vakuuttaa",
|
||||
"items": "Erä",
|
||||
"join_discord": "Liity Discord",
|
||||
"labels": "Tarra",
|
||||
"loading": "",
|
||||
"locations": "Sijainti",
|
||||
"maintenance": "Huolto",
|
||||
"name": "Nimi",
|
||||
@@ -211,14 +147,11 @@
|
||||
"password": "Salasana",
|
||||
"quantity": "Määrä",
|
||||
"read_docs": "Lue dokumentit",
|
||||
"return_home": "",
|
||||
"save": "Tallentaa",
|
||||
"search": "Etsiä",
|
||||
"sign_out": "Kirjaudu Ulos",
|
||||
"submit": "Lähettää",
|
||||
"unknown": "",
|
||||
"update": "Päivitys",
|
||||
"updating": "",
|
||||
"value": "Arvo",
|
||||
"version": "Versio: { version }",
|
||||
"welcome": "Tervetuloa, { username }"
|
||||
@@ -243,49 +176,27 @@
|
||||
"set_email": "Mikä on sähköpostisi?",
|
||||
"set_name": "Mikä sinun nimesi on?",
|
||||
"set_password": "Aseta salasana",
|
||||
"tagline": "Seuraa, Järjestä ja hallitse asioitasi.",
|
||||
"title": "",
|
||||
"toast": {
|
||||
"invalid_email": "",
|
||||
"invalid_email_password": "",
|
||||
"login_success": "",
|
||||
"problem_registering": "",
|
||||
"user_registered": ""
|
||||
}
|
||||
"tagline": "Seuraa, Järjestä ja hallitse asioitasi."
|
||||
},
|
||||
"items": {
|
||||
"add": "Lisätä",
|
||||
"advanced": "Edistyksellinen",
|
||||
"archived": "Arkistoitu",
|
||||
"asset_id": "Omaisuuserän ID",
|
||||
"associated_with_multiple": "",
|
||||
"attachment": "Liite",
|
||||
"attachments": "Liitteet",
|
||||
"changes_persisted_immediately": "Liitteiden muutokset tallennetaan välittömästi",
|
||||
"created_at": "Luotu Osoitteessa",
|
||||
"custom_fields": "Mukautetut Kentät",
|
||||
"delete_attachment_confirm": "",
|
||||
"delete_item_confirm": "",
|
||||
"description": "Kuvaus",
|
||||
"details": "Tiedot",
|
||||
"drag_and_drop": "Vedä ja pudota tiedostoja tähän tai valitse tiedostot napsauttamalla",
|
||||
"edit": {
|
||||
"edit_attachment_dialog": {
|
||||
"attachment_title": "",
|
||||
"attachment_type": "",
|
||||
"primary_photo": "",
|
||||
"primary_photo_sub": "",
|
||||
"select_type": "",
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"edit_details": "Muokkaa Tietoja",
|
||||
"field_selector": "Kentän Valitsin",
|
||||
"field_value": "Kentän Arvo",
|
||||
"first": "Ensimmäinen",
|
||||
"include_archive": "Sisällytä Arkistoidut Kohteet",
|
||||
"insured": "Vakuuttaa",
|
||||
"invalid_asset_id": "",
|
||||
"last": "Viimeinen",
|
||||
"lifetime_warranty": "Elinikäinen Takuu",
|
||||
"location": "Paikka",
|
||||
@@ -296,7 +207,6 @@
|
||||
"name": "Nimi",
|
||||
"negate_labels": "Poista Valitut Nimilaput",
|
||||
"next_page": "Seuraava Sivu",
|
||||
"no_attachments": "",
|
||||
"no_results": "Kohteita Ei Löytynyt",
|
||||
"notes": "Huomautus",
|
||||
"only_with_photo": "Vain tuotteet, joissa on valokuva",
|
||||
@@ -318,60 +228,24 @@
|
||||
"receipts": "Kuitin",
|
||||
"reset_search": "Haku nollataan",
|
||||
"results": "{ total } Tulokset",
|
||||
"select_field": "",
|
||||
"serial_number": "Sarjanumero",
|
||||
"show_advanced_view_options": "Näytä näkymän lisäasetukset",
|
||||
"sold_at": "Myyty klo",
|
||||
"sold_details": "Myydyt Tiedot",
|
||||
"sold_price": "Myyty Hinta",
|
||||
"sold_to": "Myyty",
|
||||
"sync_child_locations": "",
|
||||
"tip_1": "Sijainti-ja etikettisuodattimet käyttävät \" tai \" - toimintoa. Jos valitaan useampi kuin yksi, vain yksi on\n tarvitaan ottelu.",
|
||||
"tip_2": "\"#\"- Etuliitteellä tehdyt haut kyselevät omaisuuserän tunnusta (esimerkki \"#000-001\")",
|
||||
"tip_3": "Kenttäsuodattimet käyttävät \" tai \" - toimintoa. Jos valitaan useampi kuin yksi, tarvitaan vain yksi\n ottelu.",
|
||||
"tips": "Vihje",
|
||||
"tips_sub": "Etsi Vinkkejä",
|
||||
"toast": {
|
||||
"asset_not_found": "",
|
||||
"attachment_deleted": "",
|
||||
"attachment_updated": "",
|
||||
"attachment_uploaded": "",
|
||||
"child_items_location_no_longer_synced": "",
|
||||
"child_items_location_synced": "",
|
||||
"child_location_desync": "",
|
||||
"error_loading_parent_data": "",
|
||||
"failed_adjust_quantity": "",
|
||||
"failed_delete_attachment": "",
|
||||
"failed_delete_item": "",
|
||||
"failed_duplicate_item": "",
|
||||
"failed_load_asset": "",
|
||||
"failed_load_item": "",
|
||||
"failed_load_items": "",
|
||||
"failed_save": "",
|
||||
"failed_save_no_location": "",
|
||||
"failed_search_items": "",
|
||||
"failed_update_attachment": "",
|
||||
"failed_upload_attachment": "",
|
||||
"item_deleted": "",
|
||||
"item_saved": "",
|
||||
"quantity_cannot_negative": "",
|
||||
"sync_child_location": ""
|
||||
},
|
||||
"updated_at": "Päivitetty Osoitteessa",
|
||||
"warranty": "Takuu",
|
||||
"warranty_details": "Takuun Tiedot",
|
||||
"warranty_expires": "Takuu Päättyy"
|
||||
},
|
||||
"labels": {
|
||||
"label_delete_confirm": "",
|
||||
"no_results": "Tarroja Ei Löytynyt",
|
||||
"toast": {
|
||||
"failed_delete_label": "",
|
||||
"failed_load_label": "",
|
||||
"failed_update_label": "",
|
||||
"label_deleted": "",
|
||||
"label_updated": ""
|
||||
},
|
||||
"update_label": "Päivitä Nimiö"
|
||||
},
|
||||
"languages": {
|
||||
@@ -388,8 +262,6 @@
|
||||
"it": "Italia",
|
||||
"ja-JP": "Japani",
|
||||
"ko-KR": "Korea",
|
||||
"lb-LU": "",
|
||||
"lt-LT": "",
|
||||
"nb-NO": "Norjalainen Bokmål",
|
||||
"nl": "Hollanti",
|
||||
"pl": "Puola",
|
||||
@@ -414,15 +286,7 @@
|
||||
"child_locations": "Lasten Sijainnit",
|
||||
"collapse_tree": "Romahdus Puu",
|
||||
"expand_tree": "Laajenna Puu",
|
||||
"location_items_delete_confirm": "",
|
||||
"no_results": "Sijainteja Ei Löytynyt",
|
||||
"toast": {
|
||||
"failed_delete_location": "",
|
||||
"failed_load_location": "",
|
||||
"failed_update_location": "",
|
||||
"location_deleted": "",
|
||||
"location_updated": ""
|
||||
},
|
||||
"update_location": "Päivitä Sijainti"
|
||||
},
|
||||
"maintenance": {
|
||||
@@ -478,10 +342,7 @@
|
||||
"currency_format": "Valuutan Muoto",
|
||||
"current_password": "Nykyinen Salasana",
|
||||
"delete_account": "Poista tili",
|
||||
"delete_account_confirm": "",
|
||||
"delete_account_sub": "Poista tilisi ja kaikki siihen liittyvät tiedot. Tätä ei voi perua.",
|
||||
"delete_notifier_confirm": "",
|
||||
"display_legacy_header": "",
|
||||
"enabled": "Käyttöön",
|
||||
"example": "Esimerkiksi",
|
||||
"gen_invite": "Luo Kutsulinkki",
|
||||
@@ -491,70 +352,22 @@
|
||||
"language": "Kieli",
|
||||
"new_password": "Uusi Salasana",
|
||||
"no_notifiers": "Ilmoittajia ei ole määritetty",
|
||||
"no_override": "",
|
||||
"notifier_modal": "{ type, select, true {Edit} false {Create} other {Other}} Ilmoittaja",
|
||||
"notifiers": "Ilmoittaja",
|
||||
"notifiers_sub": "Saat ilmoituksia tulevista huoltomuistutuksista",
|
||||
"override_locale": "",
|
||||
"test": "Testi",
|
||||
"theme_settings": "Teeman Asetukset",
|
||||
"theme_settings_sub": "Teeman asetukset tallennetaan selaimesi paikalliseen tallennustilaan. Voit vaihtaa teemaa milloin tahansa. Jos olet\n jos sinulla on ongelmia teeman asettamisessa, yritä päivittää selaimesi.",
|
||||
"toast": {
|
||||
"account_deleted": "",
|
||||
"failed_change_password": "",
|
||||
"failed_create_notifier": "",
|
||||
"failed_delete_account": "",
|
||||
"failed_delete_notifier": "",
|
||||
"failed_get_currencies": "",
|
||||
"failed_test_notifier": "",
|
||||
"failed_update_group": "",
|
||||
"failed_update_notifier": "",
|
||||
"group_updated": "",
|
||||
"notifier_test_success": "",
|
||||
"password_changed": ""
|
||||
},
|
||||
"update_group": "Päivitä Ryhmä",
|
||||
"update_language": "Päivitä Kieli",
|
||||
"url": "URL",
|
||||
"user_profile": "käyttäjäprofiili",
|
||||
"user_profile_sub": "Kutsu käyttäjiä ja Hallitse tiliäsi."
|
||||
},
|
||||
"reports": {
|
||||
"label_generator": {
|
||||
"asset_end": "",
|
||||
"asset_start": "",
|
||||
"base_url": "",
|
||||
"bordered_labels": "",
|
||||
"generate_page": "",
|
||||
"input_placeholder": "",
|
||||
"instruction_1": "",
|
||||
"instruction_2": "",
|
||||
"instruction_3": "",
|
||||
"label_height": "",
|
||||
"label_width": "",
|
||||
"measure_type": "",
|
||||
"page_bottom_padding": "",
|
||||
"page_height": "",
|
||||
"page_left_padding": "",
|
||||
"page_right_padding": "",
|
||||
"page_top_padding": "",
|
||||
"page_width": "",
|
||||
"qr_code_example": "",
|
||||
"tip_1": "",
|
||||
"tip_2": "",
|
||||
"tip_3": "",
|
||||
"tips": "",
|
||||
"title": "",
|
||||
"toast": {
|
||||
"page_too_small_card": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"error": "Skannattaessa tapahtui virhe",
|
||||
"invalid_url": "Virheellinen viivakoodin URL",
|
||||
"no_sources": "Videolähteitä ei ole saatavilla",
|
||||
"permission_denied": "",
|
||||
"select_video_source": "Valitse videolähde",
|
||||
"title": "Skanneri",
|
||||
"unsupported": "Media Stream API ei ole tuettu ilman HTTPS"
|
||||
@@ -564,18 +377,15 @@
|
||||
"actions_set": {
|
||||
"ensure_ids": "Varmistetaan, että omaisuuserät ID:t",
|
||||
"ensure_ids_button": "Varmistetaan omaisuuserät ID:t",
|
||||
"ensure_ids_confirm": "",
|
||||
"ensure_ids_sub": "Varmistaa, että kaikilla varastossasi olevilla kohteilla on voimassa oleva asset_id-kenttä. Tämä tehdään etsimällä tietokannan korkein nykyinen asset_id-kenttä ja soveltamalla seuraavaa arvoa jokaiselle kohteelle, jolla on unset asset_id-kenttä. Tämä tehdään created_at-kentän järjestyksessä.",
|
||||
"ensure_import_refs": "Varmista Tuonnin Viitteet",
|
||||
"ensure_import_refs_button": "Varmista Tuonnin Viitteet",
|
||||
"ensure_import_refs_sub": "Varmistaa, että kaikilla varastossasi olevilla kohteilla on voimassa oleva import_ref-kenttä. Tämä tehdään luomalla satunnaisesti 8 merkin merkkijono jokaiselle kohteelle, jolla on unset import_ref-kenttä.",
|
||||
"set_primary_photo": "Aseta oletuskuva",
|
||||
"set_primary_photo_button": "Aseta Ensisijainen Kuva",
|
||||
"set_primary_photo_confirm": "",
|
||||
"set_primary_photo_sub": "Homebox versiossa v0.10.0 ensisijainen kuvakenttä lisättiin tyypin kuva liitteisiin. Tämä toiminto asettaa ensisijaiseksi kuvakentäksi tietokannan liitteet-taulukon ensimmäisen kuvan, jos sitä ei ole jo asetettu. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'Katso GitHub PR #576\" </a>\"",
|
||||
"zero_datetimes": "Nolla Kohteen Päivämääräajat",
|
||||
"zero_datetimes_button": "Nolla Kohteen Päivämääräajat",
|
||||
"zero_datetimes_confirm": "",
|
||||
"zero_datetimes_sub": "Palauttaa varastosi kaikkien päivämääräkenttien aika-arvon päivämäärän alkuun. Tämän tarkoituksena on korjata virhe, joka otettiin käyttöön varhaisessa vaiheessa sivuston kehitystä, joka aiheutti aika-arvon tallentamisen ajan kanssa, joka aiheutti ongelmia päivämääräkentissä, jotka näyttävät tarkkoja arvoja. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'Katso Github Ongelma #236 lisää yksityiskohtia.'</a>'"
|
||||
},
|
||||
"actions_sub": "Käytä toimintoja varastoon irtotavarana. Nämä ovat peruuttamattomia toimia. '<b>'Ole varovainen.</b>'",
|
||||
@@ -586,7 +396,6 @@
|
||||
"export_sub": "Vie Homebox CSV-standardin. Tämä vie kaikki varastossasi olevat tuotteet.",
|
||||
"import": "Tuo Varasto",
|
||||
"import_button": "Tuo varasto",
|
||||
"import_ref_confirm": "",
|
||||
"import_sub": "Tuo CSV-standardimuodon Homebox. Ilman ‘<code>'HB.import_ref’</code>‘ kolumni, tämä tulee ’<b>‘not’</b>' korvaa inventaariossasi olevia esineitä, vaan lisää vain uusia esineitä. Rivit, joissa on ‘<code>'HB.import_ref’</code>' sarakkeen tiedot yhdistetään olemassa oleviin kohteisiin, joilla on sama import_ref, jos sellainen on olemassa."
|
||||
},
|
||||
"import_export_sub": "Tuo ja vie varastosi CSV-tiedostoon ja siitä. Tämä on hyödyllistä siirrettäessä varastosi uuteen Homebox ilmentymään.",
|
||||
@@ -599,13 +408,6 @@
|
||||
"bill_of_materials_button": "Luo BOM",
|
||||
"bill_of_materials_sub": "Luo CSV-tiedoston (pilkulla erotetut arvot), joka voidaan tuoda taulukkolaskentaohjelmaan. Tämä on yhteenveto varastostasi, jossa on perustuotteet ja hintatiedot."
|
||||
},
|
||||
"reports_sub": "Luo erilaisia raportteja varastollesi.",
|
||||
"toast": {
|
||||
"asset_success": "",
|
||||
"failed_ensure_ids": "",
|
||||
"failed_ensure_import_refs": "",
|
||||
"failed_set_primary_photos": "",
|
||||
"failed_zero_datetimes": ""
|
||||
}
|
||||
"reports_sub": "Luo erilaisia raportteja varastollesi."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
"new_version_available_link": "Cliquez ici pour consulter les notes de version"
|
||||
}
|
||||
},
|
||||
"color_selector": {
|
||||
"color": "Couleur",
|
||||
"no_color_selected": "Aucune couleur sélectionnée",
|
||||
"randomize": "Couleur aléatoire"
|
||||
},
|
||||
"form": {
|
||||
"password": {
|
||||
"toggle_show": "Activer/désactiver l'affichage du mot de passe"
|
||||
@@ -115,8 +120,8 @@
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "Aucun résultat trouvé",
|
||||
"placeholder": "Sélectionner...",
|
||||
"search_placeholder": "Tapez pour rechercher..."
|
||||
"placeholder": "Sélectionner…",
|
||||
"search_placeholder": "Tapez pour rechercher…"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
@@ -130,7 +135,7 @@
|
||||
"page": "Page",
|
||||
"rows_per_page": "Lignes par page",
|
||||
"table_settings": "Paramètres du Tableau",
|
||||
"view_item": ""
|
||||
"view_item": "Afficher l'élément"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -168,7 +173,7 @@
|
||||
"select_location": "Choisir un emplacement"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "Aucun emplacement disponible. Créez votre premier emplacement avec\nle bouton `<`span class=\"link-primary\"`>`Créer`<`/span`>` dans la barre de navigation."
|
||||
"no_locations": "Aucun emplacement disponible. Créez votre premier emplacement avec\nle bouton '<span class=\"link-primary\">'Créer'</span>' dans la barre de navigation."
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
@@ -196,14 +201,14 @@
|
||||
"follow_dev": "Suivre le développeur",
|
||||
"footer": {
|
||||
"api_link": "'<a href=\"https://homebox.software/en/api/\" target=\"_blank\">'API'</a>'",
|
||||
"version_link": "'<a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/'{ version }\" target=\"_blank\"> Version : { version } Commit : { build } '</a>'"
|
||||
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Version : { version } Commit : { build } '</a>'"
|
||||
},
|
||||
"github": "Projet GitHub",
|
||||
"insured": "Assuré",
|
||||
"items": "Articles",
|
||||
"join_discord": "Rejoindre le Discord",
|
||||
"labels": "Étiquettes",
|
||||
"loading": "Chargement...",
|
||||
"loading": "Chargement…",
|
||||
"locations": "Emplacements",
|
||||
"maintenance": "Maintenance",
|
||||
"name": "Nom",
|
||||
@@ -401,6 +406,7 @@
|
||||
"th-TH": "Thaï",
|
||||
"tr": "Turc",
|
||||
"uk-UA": "Ukrainien",
|
||||
"vi-VN": "Vietnamien",
|
||||
"zh-CN": "Chinois (simplifié)",
|
||||
"zh-HK": "Chinois (Hong Kong)",
|
||||
"zh-MO": "Chinois (Macao)",
|
||||
@@ -562,6 +568,10 @@
|
||||
"tools": {
|
||||
"actions": "Actions d’inventaire",
|
||||
"actions_set": {
|
||||
"create_missing_thumbnails": "Crée les miniatures manquantes",
|
||||
"create_missing_thumbnails_button": "Crée les miniatures",
|
||||
"create_missing_thumbnails_confirm": "Êtes-vous sûr de vouloir créer les vignettes manquantes ? Cette opération peut prendre un certain temps et ne peut pas être interrompue.",
|
||||
"create_missing_thumbnails_sub": "Crée des miniatures pour toutes les pièces jointes prises en charge par la configuration actuelle. Ceci est utile pour les pièces jointes importées avant la version 0.20.0 de Homebox. Cette opération n'écrase pas les miniatures existantes, mais crée de nouvelles miniatures pour les pièces jointes sans miniature. Veuillez noter que la création des miniatures s'effectue en arrière-plan et peut prendre un certain temps.",
|
||||
"ensure_ids": "Vérifier les ID de ressources",
|
||||
"ensure_ids_button": "Vérifier les ID de ressources",
|
||||
"ensure_ids_confirm": "Êtes-vous certain de vous assurer que toutes les ressources ont une ID ? Cela peut prendre du temps et est irréversible.",
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
"components": {
|
||||
"app": {
|
||||
"create_modal": {
|
||||
"createAndAddAnother": "",
|
||||
"enter": "",
|
||||
"shift": ""
|
||||
"createAndAddAnother": "Nyomj {shiftKey} + {enterKey}t, hogy létrehozd ezt és hozzáadj egy újat.",
|
||||
"enter": "Enter",
|
||||
"shift": "Shift"
|
||||
},
|
||||
"import_dialog": {
|
||||
"change_warning": "A meglévő import_ref-fel rendelkező tételek importálásának menete megváltozott. Ha a CSV fájlban van import_ref, \nakkor a tételt felülírják a CSV fájlban található értékek.",
|
||||
"description": "Importálj egy CSV fájlt, amely tartalmazza a tételeidet, címkéidet és helyeidet. A szükséges formátumról bővebben \na dokumentációban olvashatsz.",
|
||||
"title": "Importálás CSV-fájlból",
|
||||
"toast": {
|
||||
"import_failed": "",
|
||||
"import_success": "",
|
||||
"please_select_file": ""
|
||||
"import_failed": "Sikertelen importálás. Kérlek próbáld újra később.",
|
||||
"import_success": "Sikeres importálás!",
|
||||
"please_select_file": "Kérlek válassz egy fájlt az importáláshoz."
|
||||
}
|
||||
},
|
||||
"outdated": {
|
||||
@@ -24,9 +24,16 @@
|
||||
"new_version_available_link": "Kattints ide az újdonságok megtekintéséhez"
|
||||
}
|
||||
},
|
||||
"color_selector": {
|
||||
"clear": "Szín törlése",
|
||||
"color": "Szín",
|
||||
"no_color": "Nincs szín",
|
||||
"no_color_selected": "Nincs kiválasztott szín",
|
||||
"randomize": "Véletlenszerű színezés"
|
||||
},
|
||||
"form": {
|
||||
"password": {
|
||||
"toggle_show": ""
|
||||
"toggle_show": "Jelszó megjelenítése"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
@@ -68,9 +75,9 @@
|
||||
"server_print": "Nyomtatás a szerveren",
|
||||
"titles": "Címkék",
|
||||
"toast": {
|
||||
"load_status_failed": "",
|
||||
"print_failed": "",
|
||||
"print_success": ""
|
||||
"load_status_failed": "Sikertelen állapot betöltés",
|
||||
"print_failed": "Sikertelen címke nyomtatás",
|
||||
"print_success": "Címke kinyomtatva"
|
||||
}
|
||||
},
|
||||
"page_qr_code": {
|
||||
@@ -83,40 +90,51 @@
|
||||
},
|
||||
"item": {
|
||||
"attachments_list": {
|
||||
"download": "",
|
||||
"open_new_tab": ""
|
||||
"download": "Letöltés",
|
||||
"open_new_tab": "Megnyitás új lapon"
|
||||
},
|
||||
"create_modal": {
|
||||
"delete_photo": "",
|
||||
"delete_photo": "Kép törlése",
|
||||
"item_description": "Tétel leírása",
|
||||
"item_name": "Tétel neve",
|
||||
"item_photo": "Tétel fényképe 📷",
|
||||
"item_quantity": "Tételek mennyisége",
|
||||
"parent_item": "Szülő tétel",
|
||||
"rotate_photo": "",
|
||||
"set_as_primary_photo": "",
|
||||
"product_tooltip_input_barcode": "Automatikus feltöltés kézzel megadott vonalkóddal",
|
||||
"product_tooltip_scan_barcode": "Automatikus kitöltés vonalkóddal innen: 📷",
|
||||
"rotate_photo": "Kép forgatása",
|
||||
"set_as_primary_photo": "Beállítás {isPrimary , select, true {nem } false {} other {}}elsődleges fényképként",
|
||||
"title": "Új elem létrehozása",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": "",
|
||||
"failed_load_parent": "",
|
||||
"no_canvas_support": "",
|
||||
"please_select_location": "",
|
||||
"rotate_failed": "",
|
||||
"rotate_process_failed": "",
|
||||
"some_photos_failed": "",
|
||||
"upload_failed": "",
|
||||
"upload_success": "",
|
||||
"uploading_photos": ""
|
||||
"already_creating": "Elem létrehozása már folyamatban",
|
||||
"create_failed": "Sikertelen elem létrehozás",
|
||||
"create_success": "Elem létrehozva",
|
||||
"failed_load_parent": "Szülő elem betöltése sikertelen - válaszd ki manuálisan",
|
||||
"no_canvas_support": "A böngésződ nem támogatja a canvas műveleteket",
|
||||
"please_select_location": "Válassz egy helyet.",
|
||||
"rotate_failed": "Sikertelen képforgatás: { error }",
|
||||
"rotate_process_failed": "Elforgatott kép feldolgozása sikertelen",
|
||||
"some_photos_failed": "{count, plural, =0 {Nincs feltölthető fénykép.} =1 {1 fénykép feltöltése nem sikerült.} other {Néhány fénykép feltöltése nem sikerült.}}",
|
||||
"upload_failed": "Kép feltöltése sikertelen: { photoName }",
|
||||
"upload_success": "{count, plural, =0 {Nincsenek feltöltött fényképek.} =1 {A fénykép feltöltése sikeres.} other {Minden fénykép feltöltése sikeres.}}",
|
||||
"uploading_photos": "{count, plural, =0 {Nincs feltöltendő fénykép} =1 {1 fénykép feltöltése…} other {{count} fénykép feltöltése…}}"
|
||||
},
|
||||
"upload_photos": "Fotók feltöltése",
|
||||
"uploaded": ""
|
||||
"uploaded": "Feltöltött fénykép"
|
||||
},
|
||||
"product_import": {
|
||||
"barcode": "Termék vonalkódja",
|
||||
"db_source": "Adatbázis forrás",
|
||||
"error_exception": "Kivétel történt a tétel vonalkódjának lekérése során: ",
|
||||
"error_invalid_barcode": "Érvénytelen vonalkód",
|
||||
"error_not_found": "Nem található termék a megadott vonalkóddal.",
|
||||
"search_item": "Termék keresése",
|
||||
"title": "Termék importálása"
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "",
|
||||
"placeholder": "",
|
||||
"search_placeholder": ""
|
||||
"no_results": "Nincs Találat",
|
||||
"placeholder": "Válassz…",
|
||||
"search_placeholder": "Kezdj gépelni a kereséshez…"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
@@ -130,20 +148,21 @@
|
||||
"page": "Oldal",
|
||||
"rows_per_page": "Sorok oldalanként",
|
||||
"table_settings": "Táblázatbeállítások",
|
||||
"view_item": ""
|
||||
"view_item": "Elem megtekintése"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"label_color": "Címke színe",
|
||||
"label_description": "Címke leírása",
|
||||
"label_name": "Címke neve",
|
||||
"title": "Címke létrehozása",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": "",
|
||||
"label_name_too_long": ""
|
||||
"already_creating": "Címke létrehozása már folyamatban",
|
||||
"create_failed": "Sikertelen címke létrehozás",
|
||||
"create_success": "Címke létrehozva",
|
||||
"label_name_too_long": "A címke neve nem lehet hosszabb 50 karakternél"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
@@ -156,9 +175,9 @@
|
||||
"location_name": "Hely neve",
|
||||
"title": "Új hely létrehozása",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": ""
|
||||
"already_creating": "Hely létrehozása már folyamatban",
|
||||
"create_failed": "Sikertelen hely létrehozás",
|
||||
"create_success": "Hely létrehozva"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
@@ -168,7 +187,7 @@
|
||||
"select_location": "Válassz egy helyet"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "Nincs elérhető hely. Adj hozzá új helyet a\n `<`span class=\"link-primary\"`>`Létrehozás`<`/span`>` gombbal a navigációs sávon."
|
||||
"no_locations": "Nincs elérhető hely. Adj hozzá új helyet a\n '<span class=\"link-primary\">'Létrehozás'</span>' gombbal a navigációs sávon."
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
@@ -176,6 +195,9 @@
|
||||
"shortcut_hint": "Használd a számgombokat egy művelet gyors kiválasztásához."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"api_failure": "Backend API hívás sikertelen: "
|
||||
},
|
||||
"global": {
|
||||
"add": "Hozzáadás",
|
||||
"archived": "Archivált",
|
||||
@@ -187,8 +209,8 @@
|
||||
"create_subitem": "Alelem létrehozása",
|
||||
"created": "Létrehozva",
|
||||
"delete": "Törlés",
|
||||
"delete_confirm": "",
|
||||
"demo_instance": "",
|
||||
"delete_confirm": "Biztosan törlöd ezt az elemet? ",
|
||||
"demo_instance": "Ez egy demó példány",
|
||||
"details": "Részletek",
|
||||
"duplicate": "Másolás",
|
||||
"edit": "Szerkesztés",
|
||||
@@ -196,14 +218,14 @@
|
||||
"follow_dev": "Kövesd a fejlesztőt",
|
||||
"footer": {
|
||||
"api_link": "'<a href=\"https://homebox.software/en/api/\" target=\"_blank\">'API'</a>'",
|
||||
"version_link": "'<a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/'{ version }\" target=\"_blank\"> Verzió: { version } Build: { build } '</a>'"
|
||||
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Verzió: { version } Build: { build } '</a>'"
|
||||
},
|
||||
"github": "Github projekt",
|
||||
"insured": "Biztosítva",
|
||||
"items": "Tételek",
|
||||
"join_discord": "Csatlakozz a Discordhoz",
|
||||
"labels": "Címkék",
|
||||
"loading": "",
|
||||
"loading": "Betöltés…",
|
||||
"locations": "Helyek",
|
||||
"maintenance": "Karbantartás",
|
||||
"name": "Név",
|
||||
@@ -211,14 +233,14 @@
|
||||
"password": "Jelszó",
|
||||
"quantity": "Mennyiség",
|
||||
"read_docs": "Olvasd el a dokumentációt",
|
||||
"return_home": "",
|
||||
"return_home": "Vissza a kezdőlapra",
|
||||
"save": "Mentés",
|
||||
"search": "Keresés",
|
||||
"sign_out": "Kijelentkezés",
|
||||
"submit": "Elküldés",
|
||||
"unknown": "",
|
||||
"unknown": "Ismeretlen",
|
||||
"update": "Módosítás",
|
||||
"updating": "",
|
||||
"updating": "Frissítés",
|
||||
"value": "Érték",
|
||||
"version": "Verzió: { version }",
|
||||
"welcome": "Üdv, { username }"
|
||||
@@ -244,13 +266,13 @@
|
||||
"set_name": "Mi a neved?",
|
||||
"set_password": "Állíts be egy jelszót!",
|
||||
"tagline": "Kövesd nyomon, rendszerezd és kezeld a dolgaidat.",
|
||||
"title": "",
|
||||
"title": "Rendezd és címkézd a dolgaidat",
|
||||
"toast": {
|
||||
"invalid_email": "",
|
||||
"invalid_email_password": "",
|
||||
"login_success": "",
|
||||
"problem_registering": "",
|
||||
"user_registered": ""
|
||||
"invalid_email": "E-mail cím érvénytelen",
|
||||
"invalid_email_password": "Az E-mail vagy jelszó érvénytelen",
|
||||
"login_success": "Sikeres bejelentkezés",
|
||||
"problem_registering": "Hiba a felhasználó regisztrálásakor",
|
||||
"user_registered": "Felhasználó regisztrálva"
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
@@ -258,25 +280,25 @@
|
||||
"advanced": "Haladó",
|
||||
"archived": "Archivált",
|
||||
"asset_id": "Eszközazonosító",
|
||||
"associated_with_multiple": "",
|
||||
"associated_with_multiple": "Ez az eszköz Id több elemhez van hozzárendelve",
|
||||
"attachment": "Melléklet",
|
||||
"attachments": "Mellékletek",
|
||||
"changes_persisted_immediately": "A mellékletek módosításai azonnal mentésre kerülnek",
|
||||
"created_at": "Létrehozás dátuma",
|
||||
"custom_fields": "Egyedi mezők",
|
||||
"delete_attachment_confirm": "",
|
||||
"delete_item_confirm": "",
|
||||
"delete_attachment_confirm": "Biztos törlöd ezt a mellékletet?",
|
||||
"delete_item_confirm": "Biztosan törlöd ezt az elemet?",
|
||||
"description": "Leírás",
|
||||
"details": "Részletek",
|
||||
"drag_and_drop": "Húzd ide a fájlokat, vagy kattints a fájlok kiválasztásához",
|
||||
"edit": {
|
||||
"edit_attachment_dialog": {
|
||||
"attachment_title": "",
|
||||
"attachment_type": "",
|
||||
"primary_photo": "",
|
||||
"primary_photo_sub": "",
|
||||
"select_type": "",
|
||||
"title": ""
|
||||
"attachment_title": "Melléklet Cím",
|
||||
"attachment_type": "Melléklet típusa",
|
||||
"primary_photo": "Elsődleges fénykép",
|
||||
"primary_photo_sub": "Ez a lehetőség csak fényképeknél érhető el. Csak egy fotó lehet elsődleges. Ha kiválasztod ezt a műveletet, a jelenlegi elsődleges fotó elveszti ezt a jellegét.",
|
||||
"select_type": "Válassz típust",
|
||||
"title": "Melléklet szerkesztése"
|
||||
}
|
||||
},
|
||||
"edit_details": "Részletek szerkesztése",
|
||||
@@ -285,7 +307,7 @@
|
||||
"first": "Első",
|
||||
"include_archive": "Archivált elemek belefoglalása",
|
||||
"insured": "Biztosítva",
|
||||
"invalid_asset_id": "",
|
||||
"invalid_asset_id": "Érvénytelen eszközazonosító",
|
||||
"last": "Utolsó",
|
||||
"lifetime_warranty": "Élettartam garancia",
|
||||
"location": "Hely",
|
||||
@@ -296,7 +318,7 @@
|
||||
"name": "Név",
|
||||
"negate_labels": "Címkeválasztás negálása",
|
||||
"next_page": "Következő oldal",
|
||||
"no_attachments": "",
|
||||
"no_attachments": "Nem található melléklet",
|
||||
"no_results": "Egy elem sem található",
|
||||
"notes": "Megjegyzések",
|
||||
"only_with_photo": "Csak fényképes tételek",
|
||||
@@ -318,44 +340,44 @@
|
||||
"receipts": "Számlák",
|
||||
"reset_search": "Alaphelyzet",
|
||||
"results": "{total} találat",
|
||||
"select_field": "",
|
||||
"select_field": "Válaszd ki a mezőt",
|
||||
"serial_number": "Sorozatszám",
|
||||
"show_advanced_view_options": "További beállítások megjelenítése",
|
||||
"sold_at": "Eladás dátuma",
|
||||
"sold_details": "Eladás részletei",
|
||||
"sold_price": "Eladási ár",
|
||||
"sold_to": "Vevő",
|
||||
"sync_child_locations": "",
|
||||
"sync_child_locations": "Gyermekelemek helyeinek szinkronizálása",
|
||||
"tip_1": "A hely- és címkeszűrők a „vagy” műveletet használják. Ha egynél többet választasz ki,\n bármelyik egyezése esetén megjelenik a tétel.",
|
||||
"tip_2": "A '#' előtaggal ellátott keresések egy eszközazonosítót fognak lekérdezni (például '#000-001')",
|
||||
"tip_3": "A mezőszűrők a „vagy” műveletet használják. Ha egynél többet választasz ki,\n bármelyik egyezése esetén megjelenik a tétel.",
|
||||
"tips": "Tippek",
|
||||
"tips_sub": "Tippek a kereséshez",
|
||||
"toast": {
|
||||
"asset_not_found": "",
|
||||
"attachment_deleted": "",
|
||||
"attachment_updated": "",
|
||||
"attachment_uploaded": "",
|
||||
"child_items_location_no_longer_synced": "",
|
||||
"child_items_location_synced": "",
|
||||
"child_location_desync": "",
|
||||
"error_loading_parent_data": "",
|
||||
"failed_adjust_quantity": "",
|
||||
"failed_delete_attachment": "",
|
||||
"failed_delete_item": "",
|
||||
"failed_duplicate_item": "",
|
||||
"failed_load_asset": "",
|
||||
"failed_load_item": "",
|
||||
"failed_load_items": "",
|
||||
"failed_save": "",
|
||||
"failed_save_no_location": "",
|
||||
"failed_search_items": "",
|
||||
"failed_update_attachment": "",
|
||||
"failed_upload_attachment": "",
|
||||
"item_deleted": "",
|
||||
"item_saved": "",
|
||||
"quantity_cannot_negative": "",
|
||||
"sync_child_location": ""
|
||||
"asset_not_found": "Az eszköz nem található",
|
||||
"attachment_deleted": "Melléklet törölve",
|
||||
"attachment_updated": "Melléklet frissítve",
|
||||
"attachment_uploaded": "Melléklet feltöltve",
|
||||
"child_items_location_no_longer_synced": "A gyermekelemek helye a továbbiakban nem lesz szinkronizálva ezzel a tétellel.",
|
||||
"child_items_location_synced": "A gyermekelemek helye szinkronizálva lett ezzel a tétellel",
|
||||
"child_location_desync": "A hely módosítása de-szinkronizálja a szülő helyéről",
|
||||
"error_loading_parent_data": "Hiba történt a szülőadatok betöltése során",
|
||||
"failed_adjust_quantity": "Mennyiség beállítása sikertelen",
|
||||
"failed_delete_attachment": "Melléklet törlése sikertelen",
|
||||
"failed_delete_item": "Tétel törlése sikertelen",
|
||||
"failed_duplicate_item": "Tétel másolása sikertelen",
|
||||
"failed_load_asset": "Eszköz betöltése sikertelen",
|
||||
"failed_load_item": "Tétel betöltése sikertelen",
|
||||
"failed_load_items": "Tételek betöltése sikertelen",
|
||||
"failed_save": "Tétel mentése sikertelen",
|
||||
"failed_save_no_location": "Tétel mentése sikertelen: nincs kiválasztott hely",
|
||||
"failed_search_items": "Tételek keresése sikertelen",
|
||||
"failed_update_attachment": "Melléklet frissítése sikertelen",
|
||||
"failed_upload_attachment": "Melléklet feltöltése sikertelen",
|
||||
"item_deleted": "Tétel törölve",
|
||||
"item_saved": "Tétel mentve",
|
||||
"quantity_cannot_negative": "A mennyiség nem lehet negatív",
|
||||
"sync_child_location": "A kiválasztott szülő szinkronizálja gyermekei tartózkodási helyét a sajátjával. Hely frissítve."
|
||||
},
|
||||
"updated_at": "Változtatás dátuma",
|
||||
"warranty": "Garancia",
|
||||
@@ -363,14 +385,14 @@
|
||||
"warranty_expires": "Garancia vége"
|
||||
},
|
||||
"labels": {
|
||||
"label_delete_confirm": "",
|
||||
"label_delete_confirm": "Biztos vagy benne, hogy törölni szeretnéd ezt a címkét? A művelet nem visszafordítható.",
|
||||
"no_results": "Nem található címke",
|
||||
"toast": {
|
||||
"failed_delete_label": "",
|
||||
"failed_load_label": "",
|
||||
"failed_update_label": "",
|
||||
"label_deleted": "",
|
||||
"label_updated": ""
|
||||
"failed_delete_label": "Címke törlése sikertelen",
|
||||
"failed_load_label": "Címke betöltése sikertelen",
|
||||
"failed_update_label": "Címke frissítése sikertelen",
|
||||
"label_deleted": "Címke törölve",
|
||||
"label_updated": "Címke frissítve"
|
||||
},
|
||||
"update_label": "Címke módosítása"
|
||||
},
|
||||
@@ -414,14 +436,14 @@
|
||||
"child_locations": "Tartalmazott helyek",
|
||||
"collapse_tree": "Fanézet becsukása",
|
||||
"expand_tree": "Fa kibontása",
|
||||
"location_items_delete_confirm": "",
|
||||
"location_items_delete_confirm": "Biztosan törlöd ezt a helyet és az összes elemét? Ez a művelet nem visszavonható.",
|
||||
"no_results": "Nem található hely",
|
||||
"toast": {
|
||||
"failed_delete_location": "",
|
||||
"failed_load_location": "",
|
||||
"failed_update_location": "",
|
||||
"location_deleted": "",
|
||||
"location_updated": ""
|
||||
"failed_delete_location": "Hely törlése sikertelen",
|
||||
"failed_load_location": "Hely betöltése sikertelen",
|
||||
"failed_update_location": "Hely frissítése sikertelen",
|
||||
"location_deleted": "Hely törölve",
|
||||
"location_updated": "Hely frissítve"
|
||||
},
|
||||
"update_location": "Hely módosítása"
|
||||
},
|
||||
@@ -478,10 +500,10 @@
|
||||
"currency_format": "Pénz formátum",
|
||||
"current_password": "Jelenlegi jelszó",
|
||||
"delete_account": "Fiók törlése",
|
||||
"delete_account_confirm": "",
|
||||
"delete_account_confirm": "Biztosan törlöd a fiókodat? Ha te vagy az utolsó tag a csoportodban, minden adatod törlődik. Ez a művelet nem visszafordítható.",
|
||||
"delete_account_sub": "Törlöd a fiókodat és az összes kapcsolódó adatot. Ezt a műveletet nem lehet visszavonni.",
|
||||
"delete_notifier_confirm": "",
|
||||
"display_legacy_header": "",
|
||||
"delete_notifier_confirm": "Biztos, hogy törölni akarod ezt az értesítőt?",
|
||||
"display_legacy_header": "{ currentValue, select, true {Legacy fejléc letiltása} false {Legacy fejléc engedélyezése} other {Nincs találat}}",
|
||||
"enabled": "Engedélyezve",
|
||||
"example": "Példa",
|
||||
"gen_invite": "Meghívó link létrehozása",
|
||||
@@ -500,18 +522,18 @@
|
||||
"theme_settings": "Téma Beállítások",
|
||||
"theme_settings_sub": "A témabeállítások a böngésző helyi tárhelyén tárolódnak. Bármikor megváltoztathatod a témát. Ha problémába\n ütközöl a téma beállításakor, próbáld meg frissíteni az oldalt a böngésződben.",
|
||||
"toast": {
|
||||
"account_deleted": "",
|
||||
"failed_change_password": "",
|
||||
"failed_create_notifier": "",
|
||||
"failed_delete_account": "",
|
||||
"failed_delete_notifier": "",
|
||||
"failed_get_currencies": "",
|
||||
"failed_test_notifier": "",
|
||||
"failed_update_group": "",
|
||||
"failed_update_notifier": "",
|
||||
"group_updated": "",
|
||||
"notifier_test_success": "",
|
||||
"password_changed": ""
|
||||
"account_deleted": "Fiókodat sikeresen töröltük.",
|
||||
"failed_change_password": "Jelszó megváltoztatása sikertelen.",
|
||||
"failed_create_notifier": "Értesítő létrehozása sikertelen.",
|
||||
"failed_delete_account": "Nem sikerült törölni a fiókodat.",
|
||||
"failed_delete_notifier": "Értesítő törlése sikertelen.",
|
||||
"failed_get_currencies": "Pénznemek lekérése sikertelen",
|
||||
"failed_test_notifier": "Nem sikerült tesztelni az értesítőt.",
|
||||
"failed_update_group": "Csoport frissítése sikertelen",
|
||||
"failed_update_notifier": "Nem sikerült frissíteni az értesítőt.",
|
||||
"group_updated": "Csoport frissítve",
|
||||
"notifier_test_success": "Az értesítő tesztje sikeres volt.",
|
||||
"password_changed": "A jelszóváltoztatás sikeres volt."
|
||||
},
|
||||
"update_group": "Csoport módosítása",
|
||||
"update_language": "Nyelv átállítása",
|
||||
@@ -521,40 +543,42 @@
|
||||
},
|
||||
"reports": {
|
||||
"label_generator": {
|
||||
"asset_end": "",
|
||||
"asset_start": "",
|
||||
"base_url": "",
|
||||
"bordered_labels": "",
|
||||
"generate_page": "",
|
||||
"input_placeholder": "",
|
||||
"instruction_1": "",
|
||||
"instruction_2": "",
|
||||
"instruction_3": "",
|
||||
"label_height": "",
|
||||
"label_width": "",
|
||||
"measure_type": "",
|
||||
"page_bottom_padding": "",
|
||||
"page_height": "",
|
||||
"page_left_padding": "",
|
||||
"page_right_padding": "",
|
||||
"page_top_padding": "",
|
||||
"page_width": "",
|
||||
"qr_code_example": "",
|
||||
"tip_1": "",
|
||||
"tip_2": "",
|
||||
"tip_3": "",
|
||||
"tips": "",
|
||||
"title": "",
|
||||
"asset_end": "Utolsó eszköz",
|
||||
"asset_start": "Első eszköz",
|
||||
"base_url": "Alap URL",
|
||||
"bordered_labels": "Keretes címkék",
|
||||
"generate_page": "Oldal létrehozása",
|
||||
"input_placeholder": "Írj ide",
|
||||
"instruction_1": "A Homebox Label Generator egy olyan eszköz, amely segít a Homebox-leltár címkéinek nyomtatásában. Ezeket előre\n kinyomtathatod, hogy bármikor felragaszthass egy új, még használatlan címkét",
|
||||
"instruction_2": "Ezek a címkék ezért úgy működnek, hogy egy URL QR-kódot és eszközazonosítót nyomtatnak egy címkére. Ha kikapcsoltad\n az eszközazonosítókat a Homebox beállításokban, akkor is használhatod ezt az eszközt, de nem mutat majd az eszközazonosító semmilyen tételre",
|
||||
"instruction_3": "Ez a funkció korai fejlesztési szakaszban van, és a jövőbeli kiadásokban változhat, ha visszajelzésed van, kérlek\nírd meg nekünk a '<a href=\"https://github.com/sysadminsmedia/homebox/discussions/53\">'GitHub vitafórumon'</a>'",
|
||||
"label_height": "Címke magassága",
|
||||
"label_width": "Címke szélessége",
|
||||
"measure_type": "Mértékegység",
|
||||
"page_bottom_padding": "Oldal alsó margója",
|
||||
"page_height": "Oldal magassága",
|
||||
"page_left_padding": "Oldal bal margója",
|
||||
"page_right_padding": "Oldal jobb margója",
|
||||
"page_top_padding": "Oldal felső margója",
|
||||
"page_width": "Oldalszélesség",
|
||||
"qr_code_example": "QR-kód példa",
|
||||
"tip_1": "Az alapértelmezett beállítások\n'<a href=\"https://www.avery.com/templates/5260\">'Avery 5260 címkeívek'</a>'nek felelnek meg. Ha más íveket használsz,\n módosítanod kell a beállításokat, hogy a laphoz igazodjanak.",
|
||||
"tip_2": "Ha a lapot testre szabod, a méretek hüvelykben vannak megadva. A 5260 ívek létrehozásakor feltűnt,\n hogy a hivatalosan megadott méretek nem azonosak a megfelelő szövegdoboz méretekkel.\n'<b>'Szükséged lehet néhány próbálkozásra a megfelelő méretek megtalálásához.'</b>'",
|
||||
"tip_3": "Nyomtatáskor ügyelj a következőkre:\n '<ol><li>'Állítsd a margókat 0-ra vagy Nincsre'</li><li>'Állítsd a skálázást 100% -ra'</li><li>'Tiltsd le a kétoldalas nyomtatást'</li><li>'Több oldal nyomtatása előtt nyomtass tesztoldalt'</li></ol>'",
|
||||
"tips": "Tippek",
|
||||
"title": "Címkegenerátor",
|
||||
"toast": {
|
||||
"page_too_small_card": ""
|
||||
"page_too_small_card": "Az oldal mérete túl kicsi a kártya méretéhez képest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"barcode_detected_message": "termék vonalkód észlelve",
|
||||
"barcode_fetch_data": "Termékadatok lekérése",
|
||||
"error": "Hiba történt a szkennelés közben",
|
||||
"invalid_url": "Érvénytelen vonalkód URL",
|
||||
"no_sources": "Nincs elérhető videóforrás",
|
||||
"permission_denied": "",
|
||||
"permission_denied": "A kamera engedélye megtagadva, engedélyezd a kamerához való hozzáférést a böngésző beállításaiban",
|
||||
"select_video_source": "Videóforrás kiválasztása",
|
||||
"title": "Szkenner",
|
||||
"unsupported": "A Media Stream API nem támogatott HTTPS nélkül"
|
||||
@@ -562,20 +586,24 @@
|
||||
"tools": {
|
||||
"actions": "Készletműveletek",
|
||||
"actions_set": {
|
||||
"create_missing_thumbnails": "Hiányzó bélyegképek létrehozása",
|
||||
"create_missing_thumbnails_button": "Bélyegképek létrehozása",
|
||||
"create_missing_thumbnails_confirm": "Biztosan létrehozod a hiányzó bélyegképeket? Ez eltarthat egy ideig, és nem lehet szüneteltetni.",
|
||||
"create_missing_thumbnails_sub": "Bélyegképeket hoz létre minden olyan melléklethez, melyet a jelenlegi konfiguráció támogat. Ez abban az esetben lehet hasznos, ha a mellékletek a Homebox v0.20.0 verziója előtt lettek feltöltve. A folyamat nem ír felül már létező bélyegképeket, csak újakat készít, ha a melléklet még nem rendelkezik ilyennel. Figyelem, a bélyegképeket egy háttérfolyamat generálja, és egy ideig eltarthat, amíg mind elkészülnek.",
|
||||
"ensure_ids": "Eszközazonosítók meglétének biztosítása",
|
||||
"ensure_ids_button": "Eszközazonosítók generálása",
|
||||
"ensure_ids_confirm": "",
|
||||
"ensure_ids_confirm": "Biztos elindítod az eszközazonosítók generálását? Ez eltarthat egy ideig, és nem lehet visszavonni.",
|
||||
"ensure_ids_sub": "Biztosítja, hogy a készletben lévő összes tétel rendelkezzen érvényes asset_id (eszközazonosító) mezővel. Ehhez megkeresi a legmagasabb asset_id mezőértéket az adatbázisban és minden olyan tételhez, amelynek nem beállított az asset_id mezője, rendre eggyel növelt értéket állít be. Ezt a created_at mezők értékének (a tétel létrehozásának dátuma) sorrendjében teszi.",
|
||||
"ensure_import_refs": "Importálási hivatkozások meglétének biztosítása",
|
||||
"ensure_import_refs_button": "Hivatkozások generálása",
|
||||
"ensure_import_refs_sub": "Biztosítja, hogy a készletben lévő összes tétel rendelkezzen érvényes import_ref mezővel. Véletlenszerűen generál egy 8 hosszúságú karakterláncot minden olyan tételhez, amelynél az import_ref mező üres.",
|
||||
"set_primary_photo": "Elsődleges fénykép hozzárendelése",
|
||||
"set_primary_photo_button": "Hozzárendelés",
|
||||
"set_primary_photo_confirm": "",
|
||||
"set_primary_photo_confirm": "Biztosan elindítod az elsődleges fényképek hozzárendelését? Ez eltarthat egy ideig, és nem lehet visszavonni.",
|
||||
"set_primary_photo_sub": "A Homebox v0.10.0 verziójában hozzáadtuk a fénykép típusú mellékletekhez az elsődleges fényképként történő megjelölés lehetőségét. Ezzel a művelettel a mellékletekben található első fényképet állítod be elsődleges fényképnek, ha ilyen a tételhez még nincs kiválasztva. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'Lásd az #576 GitHub PR-t'</a>'",
|
||||
"zero_datetimes": "Idő törlése a tételek dátummezőiből",
|
||||
"zero_datetimes_button": "Dátummezők javítása",
|
||||
"zero_datetimes_confirm": "",
|
||||
"zero_datetimes_confirm": "Biztosan elindítod az összes dátummező időértékének törlését? Ez eltarthat egy ideig, és nem lehet visszavonni.",
|
||||
"zero_datetimes_sub": "Visszaállítja a dátumot és időt tartalmazó mezők értékét a dátum kezdetére a teljes készletben. Ezzel javíthatsz egy olyan bugot, mely során az oldal fejlesztésének korai szakaszában az időértékek mentése a dátumok pontos megjelenítésében hibát okozott. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'Lásd a #236 Github Issue-t további részletekért.'</a>'"
|
||||
},
|
||||
"actions_sub": "Műveletek tömeges alkalmazása a készletre. Ezeket a vissza nem vonható műveleteket csak '<b>'kellő körültekintés mellett használd'</b>'.",
|
||||
@@ -586,7 +614,7 @@
|
||||
"export_sub": "Exportálja a Homebox szabványos CSV formátumát. Ez minden készletedben található tételt exportál.",
|
||||
"import": "Készlet importálása",
|
||||
"import_button": "Készlet importálása",
|
||||
"import_ref_confirm": "",
|
||||
"import_ref_confirm": "Biztos elindítod az importálási hivatkozások meglétének biztosítását? Ez eltarthat egy ideig, és nem lehet visszavonni.",
|
||||
"import_sub": "Importálja a Homebox szabványos CSV formátumát. Amennyiben nem található '<code>'HB.import_ref'</code>' oszlop a fájlban, ez '<b>'nem'</b>' ír felül létező tételeket a készletedben, csak újakat ad hozzá. Azon sorok, melyeknél a '<code>'HB.import_ref'</code>' oszlop értéke megegyezik egy létező tétel import_ref mezőjének értékével, a sor tartalma beolvad a létező tételbe."
|
||||
},
|
||||
"import_export_sub": "Készlet importálása és exportálása CSV-fájlba és CSV-fájlból. Ez hasznos lehet a készleted átmozgatásához a Homebox egy új példányába.",
|
||||
@@ -601,11 +629,12 @@
|
||||
},
|
||||
"reports_sub": "Hozz létre különböző jelentéseket a készletedhez.",
|
||||
"toast": {
|
||||
"asset_success": "",
|
||||
"failed_ensure_ids": "",
|
||||
"failed_ensure_import_refs": "",
|
||||
"failed_set_primary_photos": "",
|
||||
"failed_zero_datetimes": ""
|
||||
"asset_success": "{ results } eszköz frissítve.",
|
||||
"failed_create_missing_thumbnails": "Hiányzó bélyegképek létrehozása sikertelen.",
|
||||
"failed_ensure_ids": "Nem sikerült biztosítani az eszközazonosítók létezését.",
|
||||
"failed_ensure_import_refs": "Importálási hivatkozások meglétének biztosítása sikertelen.",
|
||||
"failed_set_primary_photos": "Nem sikerült beállítani az elsődleges fényképeket.",
|
||||
"failed_zero_datetimes": "Nem sikerült visszaállítani a dátum- és időértékeket."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"create_modal": {
|
||||
"createAndAddAnother": "",
|
||||
"enter": "",
|
||||
"shift": ""
|
||||
},
|
||||
"import_dialog": {
|
||||
"change_warning": "Ada perubahan pada logika impor untuk data dengan import_ref yang sudah ada. Jika sebuah import_ref ditemukan di file CSV,\ndata tersebut akan diperbarui menggunakan nilai-nilai yang ada di file CSV.",
|
||||
"description": "Impor file CSV yang berisi item, label, dan lokasi Anda. Lihat dokumentasi untuk informasi lebih lanjut mengenai\nformat yang diperlukan.",
|
||||
"title": "Impor CSV",
|
||||
"toast": {
|
||||
"import_failed": "",
|
||||
"import_success": "",
|
||||
"please_select_file": ""
|
||||
}
|
||||
"title": "Impor CSV"
|
||||
},
|
||||
"outdated": {
|
||||
"current_version": "Versi Terkini",
|
||||
@@ -24,11 +14,6 @@
|
||||
"new_version_available_link": "Klik di sini untuk melihat informasi rilis"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"password": {
|
||||
"toggle_show": ""
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"copy_text": {
|
||||
"documentation": "dokumentasi",
|
||||
@@ -61,62 +46,20 @@
|
||||
"yesterday": "kemaren"
|
||||
},
|
||||
"label_maker": {
|
||||
"browser_print": "",
|
||||
"confirm_description": "",
|
||||
"download": "Unduh Label",
|
||||
"print": "",
|
||||
"server_print": "",
|
||||
"titles": "",
|
||||
"toast": {
|
||||
"load_status_failed": "",
|
||||
"print_failed": "",
|
||||
"print_success": ""
|
||||
}
|
||||
"download": "Unduh Label"
|
||||
},
|
||||
"page_qr_code": {
|
||||
"page_url": "Halaman URL",
|
||||
"qr_tooltip": ""
|
||||
"page_url": "Halaman URL"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Kompleksitas kata sandi"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"attachments_list": {
|
||||
"download": "",
|
||||
"open_new_tab": ""
|
||||
},
|
||||
"create_modal": {
|
||||
"delete_photo": "",
|
||||
"item_description": "Deskripsi item",
|
||||
"item_name": "Nama item",
|
||||
"item_photo": "",
|
||||
"item_quantity": "",
|
||||
"parent_item": "",
|
||||
"rotate_photo": "",
|
||||
"set_as_primary_photo": "",
|
||||
"title": "Buat item",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": "",
|
||||
"failed_load_parent": "",
|
||||
"no_canvas_support": "",
|
||||
"please_select_location": "",
|
||||
"rotate_failed": "",
|
||||
"rotate_process_failed": "",
|
||||
"some_photos_failed": "",
|
||||
"upload_failed": "",
|
||||
"upload_success": "",
|
||||
"uploading_photos": ""
|
||||
},
|
||||
"upload_photos": "",
|
||||
"uploaded": ""
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "",
|
||||
"placeholder": "",
|
||||
"search_placeholder": ""
|
||||
"title": "Buat item"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
@@ -126,11 +69,8 @@
|
||||
"table": "Tabel"
|
||||
},
|
||||
"table": {
|
||||
"headers": "",
|
||||
"page": "Halaman",
|
||||
"rows_per_page": "Baris per halaman",
|
||||
"table_settings": "",
|
||||
"view_item": ""
|
||||
"rows_per_page": "Baris per halaman"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -138,87 +78,54 @@
|
||||
"create_modal": {
|
||||
"label_description": "Keterangan/Deskripsi",
|
||||
"label_name": "Nama",
|
||||
"title": "Buat label",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": "",
|
||||
"label_name_too_long": ""
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"select_labels": ""
|
||||
"title": "Buat label"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"location_description": "Deskripsi lokasi",
|
||||
"location_name": "Nama lokasi",
|
||||
"title": "Tambah Lokasi",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": ""
|
||||
}
|
||||
"title": "Tambah Lokasi"
|
||||
},
|
||||
"selector": {
|
||||
"no_location_found": "",
|
||||
"parent_location": "Lokasi Induk",
|
||||
"search_location": "",
|
||||
"select_location": ""
|
||||
"parent_location": "Lokasi Induk"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "Tidak ada lokasi yang tersedia. Tambahkan lokasi melalui tombol\n`<`span class=\"link-primary\"`>`Buat`<`/span`>` menu navigasi."
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
"no_results": "",
|
||||
"shortcut_hint": "Gunakan tombol angka untuk memilih."
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"add": "Tambah",
|
||||
"archived": "",
|
||||
"build": "Kompilasi: { build }",
|
||||
"cancel": "",
|
||||
"confirm": "Konfirmasi",
|
||||
"create": "Buat",
|
||||
"create_and_add": "Buat dan Tambah Baru",
|
||||
"create_subitem": "",
|
||||
"created": "Behasil dibuat",
|
||||
"delete": "Hapus",
|
||||
"delete_confirm": "",
|
||||
"demo_instance": "",
|
||||
"details": "Detail",
|
||||
"duplicate": "Duplikat",
|
||||
"edit": "Sunting",
|
||||
"email": "Email",
|
||||
"follow_dev": "Ikuti Pengembang",
|
||||
"footer": {
|
||||
"api_link": "",
|
||||
"version_link": ""
|
||||
},
|
||||
"github": "Github project",
|
||||
"insured": "",
|
||||
"items": "Barang",
|
||||
"join_discord": "Bergabunglah dengan Discord",
|
||||
"labels": "Label",
|
||||
"loading": "",
|
||||
"locations": "Lokasi",
|
||||
"maintenance": "Perbaikan",
|
||||
"name": "Nama",
|
||||
"navigate": "Navigasi",
|
||||
"password": "Kata Sandi",
|
||||
"quantity": "",
|
||||
"read_docs": "Baca Dokumen",
|
||||
"return_home": "",
|
||||
"save": "Simpan",
|
||||
"search": "Cari",
|
||||
"sign_out": "Keluar",
|
||||
"submit": "Kirim",
|
||||
"unknown": "",
|
||||
"update": "Perbaharui",
|
||||
"updating": "",
|
||||
"value": "Nilai",
|
||||
"version": "Versi:{ version }",
|
||||
"welcome": "Selamay datang, { username }"
|
||||
@@ -243,49 +150,27 @@
|
||||
"set_email": "Apa email Anda?",
|
||||
"set_name": "Apa nama anda?",
|
||||
"set_password": "Password Anda",
|
||||
"tagline": "Lacak, Atur, dan Kelola Barang-barangmu.",
|
||||
"title": "",
|
||||
"toast": {
|
||||
"invalid_email": "",
|
||||
"invalid_email_password": "",
|
||||
"login_success": "",
|
||||
"problem_registering": "",
|
||||
"user_registered": ""
|
||||
}
|
||||
"tagline": "Lacak, Atur, dan Kelola Barang-barangmu."
|
||||
},
|
||||
"items": {
|
||||
"add": "Tambah",
|
||||
"advanced": "Tingkat Lanjut",
|
||||
"archived": "Diarsipkan",
|
||||
"asset_id": "ID Aset",
|
||||
"associated_with_multiple": "",
|
||||
"attachment": "Lampiran",
|
||||
"attachments": "Lampiran",
|
||||
"changes_persisted_immediately": "Perubahan lampiran akan segera disimpan",
|
||||
"created_at": "Dibuat Pada",
|
||||
"custom_fields": "Informasi Tambahan",
|
||||
"delete_attachment_confirm": "",
|
||||
"delete_item_confirm": "",
|
||||
"description": "Deskripsi",
|
||||
"details": "Detail",
|
||||
"drag_and_drop": "Seret dan lepas file di sini atau klik untuk memilih file",
|
||||
"edit": {
|
||||
"edit_attachment_dialog": {
|
||||
"attachment_title": "",
|
||||
"attachment_type": "",
|
||||
"primary_photo": "",
|
||||
"primary_photo_sub": "",
|
||||
"select_type": "",
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"edit_details": "Edit Detail",
|
||||
"field_selector": "Selektor",
|
||||
"field_value": "Nilai",
|
||||
"first": "Pertama",
|
||||
"include_archive": "Sertakan Item yang Diarsipkan",
|
||||
"insured": "Diasuransikan",
|
||||
"invalid_asset_id": "",
|
||||
"last": "Terakhir",
|
||||
"lifetime_warranty": "Garansi seumur hidup",
|
||||
"location": "Lokasi",
|
||||
@@ -296,7 +181,6 @@
|
||||
"name": "Nama",
|
||||
"negate_labels": "Negasikan Label yang Dipilih",
|
||||
"next_page": "Halaman Berikutnya",
|
||||
"no_attachments": "",
|
||||
"no_results": "Item tidak ditemukan",
|
||||
"notes": "Catatan",
|
||||
"only_with_photo": "Hanya item dengan foto",
|
||||
@@ -318,88 +202,42 @@
|
||||
"receipts": "Resi",
|
||||
"reset_search": "Reset Pencarian",
|
||||
"results": "{ total } Hasil",
|
||||
"select_field": "",
|
||||
"serial_number": "Nomor Seri",
|
||||
"show_advanced_view_options": "Tampilkan opsi lanjutan",
|
||||
"sold_at": "Dijual Pada",
|
||||
"sold_details": "Detail Penjualan",
|
||||
"sold_price": "Harga Jual",
|
||||
"sold_to": "Dijual Kepada",
|
||||
"sync_child_locations": "",
|
||||
"tip_1": "Filter untuk lokasi dan label bekerja dengan operasi 'OR'. Artinya, jika Anda memilih lebih dari satu filter, hanya satu\nfilter yang harus cocok agar data ditampilkan.",
|
||||
"tip_2": "Pencarian yang diawali dengan '#'' akan meminta ID aset (contoh '#000 -001 ')",
|
||||
"tip_3": "Filter untuk lokasi dan label bekerja dengan operasi 'OR'. Artinya, jika Anda memilih lebih dari satu filter, hanya satu\nfilter yang harus cocok agar data ditampilkan.",
|
||||
"tips": "Anjuran",
|
||||
"tips_sub": "Tips Pencarian",
|
||||
"toast": {
|
||||
"asset_not_found": "",
|
||||
"attachment_deleted": "",
|
||||
"attachment_updated": "",
|
||||
"attachment_uploaded": "",
|
||||
"child_items_location_no_longer_synced": "",
|
||||
"child_items_location_synced": "",
|
||||
"child_location_desync": "",
|
||||
"error_loading_parent_data": "",
|
||||
"failed_adjust_quantity": "",
|
||||
"failed_delete_attachment": "",
|
||||
"failed_delete_item": "",
|
||||
"failed_duplicate_item": "",
|
||||
"failed_load_asset": "",
|
||||
"failed_load_item": "",
|
||||
"failed_load_items": "",
|
||||
"failed_save": "",
|
||||
"failed_save_no_location": "",
|
||||
"failed_search_items": "",
|
||||
"failed_update_attachment": "",
|
||||
"failed_upload_attachment": "",
|
||||
"item_deleted": "",
|
||||
"item_saved": "",
|
||||
"quantity_cannot_negative": "",
|
||||
"sync_child_location": ""
|
||||
},
|
||||
"updated_at": "Diperbarui pada",
|
||||
"warranty": "Garansi",
|
||||
"warranty_details": "Rincian Garansi",
|
||||
"warranty_expires": "Garansi Kedaluwarsa"
|
||||
},
|
||||
"labels": {
|
||||
"label_delete_confirm": "",
|
||||
"no_results": "Tidak Ditemukan Label",
|
||||
"toast": {
|
||||
"failed_delete_label": "",
|
||||
"failed_load_label": "",
|
||||
"failed_update_label": "",
|
||||
"label_deleted": "",
|
||||
"label_updated": ""
|
||||
},
|
||||
"update_label": "Perbarui Label"
|
||||
},
|
||||
"languages": {
|
||||
"ca": "Catalan",
|
||||
"cs-CZ": "",
|
||||
"de": "Jerman",
|
||||
"en": "Bahasa Inggris",
|
||||
"es": "Spanyol",
|
||||
"fi-FI": "",
|
||||
"fr": "Prancis",
|
||||
"hu": "Hungaria",
|
||||
"id-ID": "",
|
||||
"it": "Italia",
|
||||
"ja-JP": "Jepang",
|
||||
"ko-KR": "",
|
||||
"lb-LU": "",
|
||||
"lt-LT": "",
|
||||
"nb-NO": "",
|
||||
"nl": "Belanda",
|
||||
"pl": "Polandia",
|
||||
"pt-BR": "Portugis (Brasil)",
|
||||
"pt-PT": "Bahasa Portugis (Portugal)",
|
||||
"ru": "Rusia",
|
||||
"sl": "Bahasa Slovenia",
|
||||
"sq-AL": "",
|
||||
"sv": "Swedia",
|
||||
"ta-IN": "",
|
||||
"th-TH": "",
|
||||
"tr": "Turki",
|
||||
"uk-UA": "Ukraina",
|
||||
"zh-CN": "Mandarin (Disederhanakan)",
|
||||
@@ -413,16 +251,7 @@
|
||||
"locations": {
|
||||
"child_locations": "Lokasi Turunan",
|
||||
"collapse_tree": "Ciutkan",
|
||||
"expand_tree": "",
|
||||
"location_items_delete_confirm": "",
|
||||
"no_results": "Lokasi tidak ditemukan",
|
||||
"toast": {
|
||||
"failed_delete_location": "",
|
||||
"failed_load_location": "",
|
||||
"failed_update_location": "",
|
||||
"location_deleted": "",
|
||||
"location_updated": ""
|
||||
},
|
||||
"update_location": "Perbarui Lokasi"
|
||||
},
|
||||
"maintenance": {
|
||||
@@ -468,7 +297,6 @@
|
||||
"locations": "Lokasi",
|
||||
"maintenance": "Pemeliharaan",
|
||||
"profile": "Profil",
|
||||
"scanner": "",
|
||||
"search": "Cari",
|
||||
"tools": "Tools"
|
||||
},
|
||||
@@ -478,10 +306,7 @@
|
||||
"currency_format": "Format Mata uang",
|
||||
"current_password": "Kata sandi saat ini",
|
||||
"delete_account": "Hapus Akun",
|
||||
"delete_account_confirm": "",
|
||||
"delete_account_sub": "Hapus akun Anda dan semua data terkait.",
|
||||
"delete_notifier_confirm": "",
|
||||
"display_legacy_header": "",
|
||||
"enabled": "Tersedia",
|
||||
"example": "Contoh",
|
||||
"gen_invite": "Buat Tautan Undangan",
|
||||
@@ -491,91 +316,32 @@
|
||||
"language": "Bahasa",
|
||||
"new_password": "Kata Sandi Baru",
|
||||
"no_notifiers": "Tidak ada notifier yang dikonfigurasi",
|
||||
"no_override": "",
|
||||
"notifier_modal": "{ type, select, true {Ubah} false {Buat} other {Lainnya}} Notifier",
|
||||
"notifiers": "Notifier",
|
||||
"notifiers_sub": "Dapatkan pemberitahuan untuk pengingat pemeliharaan mendatang",
|
||||
"override_locale": "",
|
||||
"test": "Pengujian",
|
||||
"theme_settings": "Pengaturan Tampilan",
|
||||
"theme_settings_sub": "Pengaturan tema disimpan di penyimpanan lokal browser Anda. Anda dapat mengubah tema kapan saja. Jika Anda\nmengalami masalah dalam mengatur tema, coba refresh browser Anda.",
|
||||
"toast": {
|
||||
"account_deleted": "",
|
||||
"failed_change_password": "",
|
||||
"failed_create_notifier": "",
|
||||
"failed_delete_account": "",
|
||||
"failed_delete_notifier": "",
|
||||
"failed_get_currencies": "",
|
||||
"failed_test_notifier": "",
|
||||
"failed_update_group": "",
|
||||
"failed_update_notifier": "",
|
||||
"group_updated": "",
|
||||
"notifier_test_success": "",
|
||||
"password_changed": ""
|
||||
},
|
||||
"update_group": "Perbarui Grup",
|
||||
"update_language": "Perbarui Bahasa",
|
||||
"url": "URL",
|
||||
"user_profile": "Profil Pengguna",
|
||||
"user_profile_sub": "Undang pengguna, dan kelola akun Anda."
|
||||
},
|
||||
"reports": {
|
||||
"label_generator": {
|
||||
"asset_end": "",
|
||||
"asset_start": "",
|
||||
"base_url": "",
|
||||
"bordered_labels": "",
|
||||
"generate_page": "",
|
||||
"input_placeholder": "",
|
||||
"instruction_1": "",
|
||||
"instruction_2": "",
|
||||
"instruction_3": "",
|
||||
"label_height": "",
|
||||
"label_width": "",
|
||||
"measure_type": "",
|
||||
"page_bottom_padding": "",
|
||||
"page_height": "",
|
||||
"page_left_padding": "",
|
||||
"page_right_padding": "",
|
||||
"page_top_padding": "",
|
||||
"page_width": "",
|
||||
"qr_code_example": "",
|
||||
"tip_1": "",
|
||||
"tip_2": "",
|
||||
"tip_3": "",
|
||||
"tips": "",
|
||||
"title": "",
|
||||
"toast": {
|
||||
"page_too_small_card": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"error": "",
|
||||
"invalid_url": "",
|
||||
"no_sources": "",
|
||||
"permission_denied": "",
|
||||
"select_video_source": "",
|
||||
"title": "",
|
||||
"unsupported": ""
|
||||
},
|
||||
"tools": {
|
||||
"actions": "Kelola Inventaris",
|
||||
"actions_set": {
|
||||
"ensure_ids": "Pastikan ID Aset",
|
||||
"ensure_ids_button": "Pastikan ID Aset",
|
||||
"ensure_ids_confirm": "",
|
||||
"ensure_ids_sub": "Memastikan semua item dalam inventaris Anda memiliki kolom asset_id yang valid. Hal ini dilakukan dengan mencari nilai asset_id tertinggi saat ini dalam database dan menerapkan nilai berikutnya ke setiap item yang kolom asset_id-nya belum diatur. Proses ini diurutkan berdasarkan kolom created_at.",
|
||||
"ensure_import_refs": "Pastikan Ref Impor",
|
||||
"ensure_import_refs_button": "Pastikan Ref Impor",
|
||||
"ensure_import_refs_sub": "Memastikan semua item dalam inventaris Anda memiliki kolom import_ref yang valid. Hal ini dilakukan dengan membuat string 8 karakter acak untuk setiap item yang kolom import_ref-nya belum diatur.",
|
||||
"set_primary_photo": "Atur Foto Utama",
|
||||
"set_primary_photo_button": "Atur Foto Utama",
|
||||
"set_primary_photo_confirm": "",
|
||||
"set_primary_photo_sub": "Pada Homebox versi 0.10.0, kolom gambar utama ditambahkan ke lampiran bertipe foto. Ini akan mengatur kolom gambar utama ke gambar pertama dalam lampiran di database, jika belum diatur. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'See GitHub PR #576'</a>'",
|
||||
"zero_datetimes": "Format Waktu Kosong",
|
||||
"zero_datetimes_button": "Format Waktu Kosong",
|
||||
"zero_datetimes_confirm": "",
|
||||
"zero_datetimes_sub": "Mengatur ulang nilai waktu untuk semua bidang tanggal dan waktu di inventaris Anda ke awal tanggal. Ini untuk memperbaiki bug yang muncul di awal pengembangan situs yang menyebabkan nilai waktu disimpan bersama waktu, yang mengakibatkan masalah pada tampilan nilai tanggal yang akurat. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'See Github Issue #236 for more details.'</a>'"
|
||||
},
|
||||
"actions_sub": "Terapkan tindakan ke inventaris Anda secara massal. Tindakan ini tidak dapat diurungkan. <b>Hati-hati.</b>",
|
||||
@@ -586,7 +352,6 @@
|
||||
"export_sub": "Mengekspor semua item ke file CSV dengan format standar Homebox.",
|
||||
"import": "Impor Inventaris",
|
||||
"import_button": "Impor Inventaris",
|
||||
"import_ref_confirm": "",
|
||||
"import_sub": "Mengimpor CSV dengan format standar Homebox. Tanpa kolom '<code>HB.import_ref</code>', impor ini <b>tidak</b> akan menimpa item yang sudah ada di inventaris Anda, hanya menambahkan item baru. Baris dengan kolom '<code>HB.import_ref</code>' akan digabungkan ke item yang sudah ada dengan import_ref yang sama, jika ada."
|
||||
},
|
||||
"import_export_sub": "Impor dan ekspor inventaris Anda ke dan dari file CSV. Ini berguna untuk memigrasikan inventaris Anda ke instance Homebox yang baru.",
|
||||
@@ -599,13 +364,6 @@
|
||||
"bill_of_materials_button": "Download Daftar Inventaris",
|
||||
"bill_of_materials_sub": "Menghasilkan file CSV (Comma Separated Values) yang dapat diimpor ke program spreadsheet. Ini adalah ringkasan inventaris Anda dengan info dasar beserta harganya."
|
||||
},
|
||||
"reports_sub": "Buat laporan yang berbeda untuk inventaris Anda.",
|
||||
"toast": {
|
||||
"asset_success": "",
|
||||
"failed_ensure_ids": "",
|
||||
"failed_ensure_import_refs": "",
|
||||
"failed_set_primary_photos": "",
|
||||
"failed_zero_datetimes": ""
|
||||
}
|
||||
"reports_sub": "Buat laporan yang berbeda untuk inventaris Anda."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,13 @@
|
||||
"new_version_available_link": "Clicca qui per visualizzare le note di rilascio"
|
||||
}
|
||||
},
|
||||
"color_selector": {
|
||||
"clear": "Pulisci il colore",
|
||||
"color": "Colore",
|
||||
"no_color": "Nessun colore",
|
||||
"no_color_selected": "Nessun colore selezionato",
|
||||
"randomize": "Colore casuale"
|
||||
},
|
||||
"form": {
|
||||
"password": {
|
||||
"toggle_show": "Attiva/disattiva visualizzazione password"
|
||||
@@ -93,6 +100,7 @@
|
||||
"item_photo": "Foto dell'articolo 📷",
|
||||
"item_quantity": "Quantità Articoli",
|
||||
"parent_item": "Articolo principale",
|
||||
"product_tooltip_scan_barcode": "Riempimento automatico con un codice a barre da 📷",
|
||||
"rotate_photo": "Ruota foto",
|
||||
"set_as_primary_photo": "Imposta come { isPrimary, select, true {non} false {} other {}} foto principale",
|
||||
"title": "Crea Articolo",
|
||||
@@ -113,10 +121,17 @@
|
||||
"upload_photos": "Carica Foto",
|
||||
"uploaded": "Foto caricata"
|
||||
},
|
||||
"product_import": {
|
||||
"barcode": "Codice a barre del prodotto",
|
||||
"error_exception": "Si è verificato un errore durante il recupero del codice a barre dell'articolo: ",
|
||||
"error_invalid_barcode": "Il codice a barre fornito non è valido",
|
||||
"search_item": "Cerca prodotto",
|
||||
"title": "Importa prodotto"
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "Nessun risultato trovato",
|
||||
"placeholder": "Seleziona...",
|
||||
"search_placeholder": "Digita per cercare"
|
||||
"placeholder": "Seleziona…",
|
||||
"search_placeholder": "Scrivi per cercare…"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
@@ -168,7 +183,7 @@
|
||||
"select_location": "Seleziona una posizione"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "Nessuna posizione disponibile. Aggiungi nuove posizioni mediante il pulsante\n`<`span class=\"link-primary\"`>`Crea`<`/span`>` nella barra di navigazione."
|
||||
"no_locations": "Nessuna posizione disponibile. Aggiungi nuove posizioni mediante il pulsante\n'<span class=\"link-primary\">'Crea'</span>' nella barra di navigazione."
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
@@ -196,14 +211,14 @@
|
||||
"follow_dev": "Segui lo Sviluppatore",
|
||||
"footer": {
|
||||
"api_link": "'<a href=\"https://homebox.software/en/api/\" target=\"_blank\">'API'</a>'",
|
||||
"version_link": "'<a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/'{ version }\" target=\"_blank\"> Versione: { version } Build: { build } '</a>'"
|
||||
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Versione: { version } Build: { build } '</a>'"
|
||||
},
|
||||
"github": "Progetto GitHub",
|
||||
"insured": "Assicurato",
|
||||
"items": "Articoli",
|
||||
"join_discord": "Unisciti a Discord",
|
||||
"labels": "Etichette",
|
||||
"loading": "Caricamento...",
|
||||
"loading": "Caricamento…",
|
||||
"locations": "Posizioni",
|
||||
"maintenance": "Manutenzione",
|
||||
"name": "Nome",
|
||||
@@ -280,12 +295,12 @@
|
||||
}
|
||||
},
|
||||
"edit_details": "Modifica dettagli",
|
||||
"field_selector": "Campo Selezione",
|
||||
"field_selector": "Selezione in base ai campi",
|
||||
"field_value": "Campo valore",
|
||||
"first": "Primo",
|
||||
"include_archive": "Includi Articoli Archiviati",
|
||||
"insured": "Assicurato",
|
||||
"invalid_asset_id": "ID dell'asset non valido.",
|
||||
"invalid_asset_id": "ID dell'asset non valido",
|
||||
"last": "Ultimo",
|
||||
"lifetime_warranty": "Garanzia a vita",
|
||||
"location": "Luogo",
|
||||
@@ -311,7 +326,7 @@
|
||||
"purchase_date": "Data di acquisto",
|
||||
"purchase_details": "Dettagli dell'acquisto",
|
||||
"purchase_price": "Prezzo di acquisto",
|
||||
"purchased_from": "Acqistato da",
|
||||
"purchased_from": "Acquistato da",
|
||||
"quantity": "Quantità",
|
||||
"query_id": "ID dell'Asset in Ricerca: { id }",
|
||||
"receipt": "Ricevuta",
|
||||
@@ -321,7 +336,7 @@
|
||||
"select_field": "Seleziona un campo",
|
||||
"serial_number": "Numero seriale",
|
||||
"show_advanced_view_options": "Mostra opzioni di visualizzazione avanzate",
|
||||
"sold_at": "Venduto su",
|
||||
"sold_at": "Venduto il",
|
||||
"sold_details": "Dettagli di vendita",
|
||||
"sold_price": "Prezzo di vendita",
|
||||
"sold_to": "Venduto a",
|
||||
@@ -332,49 +347,19 @@
|
||||
"tips": "Suggerimenti",
|
||||
"tips_sub": "Suggerimenti per la Ricerca",
|
||||
"toast": {
|
||||
"asset_not_found": "",
|
||||
"attachment_deleted": "",
|
||||
"attachment_updated": "",
|
||||
"attachment_uploaded": "",
|
||||
"child_items_location_no_longer_synced": "",
|
||||
"child_items_location_synced": "",
|
||||
"child_location_desync": "",
|
||||
"error_loading_parent_data": "",
|
||||
"failed_adjust_quantity": "",
|
||||
"failed_delete_attachment": "",
|
||||
"failed_delete_item": "",
|
||||
"failed_duplicate_item": "",
|
||||
"failed_load_asset": "",
|
||||
"failed_load_item": "",
|
||||
"failed_load_items": "",
|
||||
"failed_save": "",
|
||||
"failed_save_no_location": "",
|
||||
"failed_search_items": "",
|
||||
"failed_update_attachment": "",
|
||||
"failed_upload_attachment": "",
|
||||
"item_deleted": "",
|
||||
"item_saved": "",
|
||||
"quantity_cannot_negative": "",
|
||||
"sync_child_location": ""
|
||||
"quantity_cannot_negative": "La quantità non può essere negativa"
|
||||
},
|
||||
"updated_at": "Aggiornato Il",
|
||||
"warranty": "Garanzia",
|
||||
"warranty_details": "Dettagli garanzia",
|
||||
"warranty_expires": "Garanzia scaduta"
|
||||
"warranty_expires": "La garanzia scade il"
|
||||
},
|
||||
"labels": {
|
||||
"label_delete_confirm": "",
|
||||
"no_results": "Nessuna etichetta trovata",
|
||||
"toast": {
|
||||
"failed_delete_label": "",
|
||||
"failed_load_label": "",
|
||||
"failed_update_label": "",
|
||||
"label_deleted": "",
|
||||
"label_updated": ""
|
||||
},
|
||||
"update_label": "Aggiorna etichetta"
|
||||
},
|
||||
"languages": {
|
||||
"bs-BA": "Bosniaco (Bosnia ed Erzegovina)",
|
||||
"ca": "Catalano",
|
||||
"cs-CZ": "Ceco",
|
||||
"de": "Tedesco",
|
||||
@@ -401,6 +386,7 @@
|
||||
"th-TH": "Tailandese",
|
||||
"tr": "Turco",
|
||||
"uk-UA": "Ucraino",
|
||||
"vi-VN": "Vietnamita",
|
||||
"zh-CN": "Cinese (semplificato)",
|
||||
"zh-HK": "Cinese Mandarino",
|
||||
"zh-MO": "Cinese (Macao)",
|
||||
@@ -414,15 +400,7 @@
|
||||
"child_locations": "Ubicazione figlia",
|
||||
"collapse_tree": "Contrai albero",
|
||||
"expand_tree": "Espandi albero",
|
||||
"location_items_delete_confirm": "",
|
||||
"no_results": "Nessuna posizione trovata",
|
||||
"toast": {
|
||||
"failed_delete_location": "",
|
||||
"failed_load_location": "",
|
||||
"failed_update_location": "",
|
||||
"location_deleted": "",
|
||||
"location_updated": ""
|
||||
},
|
||||
"update_location": "Aggiorna ubicazione"
|
||||
},
|
||||
"maintenance": {
|
||||
@@ -478,9 +456,7 @@
|
||||
"currency_format": "Formato Valuta",
|
||||
"current_password": "Password Corrente",
|
||||
"delete_account": "Elimina Account",
|
||||
"delete_account_confirm": "",
|
||||
"delete_account_sub": "Elimina il tuo account e tutti i dati associati. Questa operazione non può essere annullata.",
|
||||
"delete_notifier_confirm": "",
|
||||
"display_legacy_header": "{ currentValue, select, true {Disable Legacy Header} false {Enable Legacy Header} other {Not Hit}}",
|
||||
"enabled": "Abilitato",
|
||||
"example": "Esempio",
|
||||
@@ -499,20 +475,6 @@
|
||||
"test": "Test",
|
||||
"theme_settings": "Impostazioni Tema",
|
||||
"theme_settings_sub": "Le impostazioni del tema sono memorizzate nella memoria locale del tuo browser. Puoi cambiare il tema \nin qualsiasi momento. Se hai problemi a impostare il tuo tema, prova a ricaricare la pagina.",
|
||||
"toast": {
|
||||
"account_deleted": "",
|
||||
"failed_change_password": "",
|
||||
"failed_create_notifier": "",
|
||||
"failed_delete_account": "",
|
||||
"failed_delete_notifier": "",
|
||||
"failed_get_currencies": "",
|
||||
"failed_test_notifier": "",
|
||||
"failed_update_group": "",
|
||||
"failed_update_notifier": "",
|
||||
"group_updated": "",
|
||||
"notifier_test_success": "",
|
||||
"password_changed": ""
|
||||
},
|
||||
"update_group": "Aggiorna Gruppo",
|
||||
"update_language": "Aggiorna Lingua",
|
||||
"url": "URL",
|
||||
@@ -521,33 +483,13 @@
|
||||
},
|
||||
"reports": {
|
||||
"label_generator": {
|
||||
"asset_end": "",
|
||||
"asset_start": "",
|
||||
"base_url": "",
|
||||
"bordered_labels": "",
|
||||
"generate_page": "",
|
||||
"input_placeholder": "",
|
||||
"instruction_1": "",
|
||||
"instruction_2": "",
|
||||
"instruction_3": "",
|
||||
"label_height": "",
|
||||
"label_width": "",
|
||||
"measure_type": "",
|
||||
"page_bottom_padding": "",
|
||||
"page_height": "Altezza della pagina",
|
||||
"page_left_padding": "Spaziatura Sinistra",
|
||||
"page_right_padding": "Spaziatura Destra",
|
||||
"page_top_padding": "Spaziatura in alto",
|
||||
"page_width": "Larghezza pagina",
|
||||
"qr_code_example": "Esempio di codice QR",
|
||||
"tip_1": "Le impostazioni predefinite qui sono configurate per i\n\n '<a href=\"https://www.avery.com/templates/5260\">'fogli di etichette ''Avery 5260 '</a>'. Se stai utilizzando un foglio differente,\n\n devi modificare le impostazioni affinchè corrispondano al tuo foglio.",
|
||||
"tip_2": "",
|
||||
"tip_3": "",
|
||||
"tips": "",
|
||||
"title": "",
|
||||
"toast": {
|
||||
"page_too_small_card": ""
|
||||
}
|
||||
"tip_1": "Le impostazioni predefinite qui sono configurate per i\n'<a href=\"https://www.avery.com/templates/5260\">'fogli di etichette ''Avery 5260 '</a>'. Se stai utilizzando un foglio differente,\n devi modificare le impostazioni affinchè corrispondano al tuo foglio."
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
@@ -564,18 +506,15 @@
|
||||
"actions_set": {
|
||||
"ensure_ids": "Verifica ID delle risorse",
|
||||
"ensure_ids_button": "Verifica ID delle risorse",
|
||||
"ensure_ids_confirm": "",
|
||||
"ensure_ids_sub": "Garantisce che tutti gli articoli nel tuo inventario abbiano un campo asset_id valido. Questo viene fatto trovando il campo asset_id corrente più alto nel database e applicando il valore successivo a ogni articolo che ha un campo asset_id non impostato. Questo viene fatto per il campo created_at.",
|
||||
"ensure_import_refs": "Verifica riferimenti di importazione",
|
||||
"ensure_import_refs_button": "Verifica riferimenti di importazione",
|
||||
"ensure_import_refs_sub": "Verifica che tutti gli articoli nel tuo inventario abbiano un campo import_ref valido. Questo viene fatto generando in modo casuale una stringa di 8 caratteri per ogni articolo che ha un campo import_ref non impostato.",
|
||||
"set_primary_photo": "Imposta foto principale",
|
||||
"set_primary_photo_button": "Imposta immagine principale",
|
||||
"set_primary_photo_confirm": "",
|
||||
"set_primary_photo_sub": "Nella versione v0.10.0 di Homebox, il campo immagine principale è stato aggiunto agli allegati di tipo foto. Questa azione imposterà il campo immagine principale alla prima immagine nella matrice allegati nel database, se non è già impostato. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'Vedi GitHub PR #576'</a>'",
|
||||
"zero_datetimes": "Azzera Data e Orario articolo",
|
||||
"zero_datetimes_button": "Azzera Date e Ora articolo",
|
||||
"zero_datetimes_confirm": "",
|
||||
"zero_datetimes_sub": "Reimposta il valore dell'ora per tutti i campi data e ora dell'inventario all'inizio della data. Questo è per correggere un bug che è stato introdotto all'inizio dello sviluppo del sito che ha causato il valore di orario memorizzato con il tempo che ha causato problemi con i campi data visualizzazione dei valori esatti. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'Vedi Github Issue #236 per maggiori dettagli.'</a>'"
|
||||
},
|
||||
"actions_sub": "Applica Azioni massive al tuo inventario. Questo sono azioni irreversibili. '<b>'Presta attenzione.'</b>'",
|
||||
@@ -586,7 +525,6 @@
|
||||
"export_sub": "Esporta il formato CSV standard per Homebox. Questo esporterà tutti gli articoli del tuo inventario.",
|
||||
"import": "Importa Inventario",
|
||||
"import_button": "Importa Inventario",
|
||||
"import_ref_confirm": "",
|
||||
"import_sub": "Importa il formato CSV standard per Homebox. Senza una colonna '<code>'HB.import_ref'</code>' questo '<b>'non'</b>' sovrascriverà gli articoli esistenti nel tuo inventario, aggiungerà solamente nuovi articoli. Le righe con una colonna '<code>'HB.import_ref'</code>' saranno unite agli articoli esistenti con lo stesso import_ref, se presente."
|
||||
},
|
||||
"import_export_sub": "Importa ed esporta il tuo inventario da e verso un file CSV. Questo è utile per migrare il tuo inventario verso una nuova istanza di Homebox.",
|
||||
@@ -599,13 +537,6 @@
|
||||
"bill_of_materials_button": "Genera BOM",
|
||||
"bill_of_materials_sub": "Genera un file CSV (Valori Separati dalla Virgola) che può essere importato in un foglio di calcolo. Questo è un sommario del tuo inventario con informazioni di base su articoli e prezzi."
|
||||
},
|
||||
"reports_sub": "Genera diversi report per il tuo inventario.",
|
||||
"toast": {
|
||||
"asset_success": "",
|
||||
"failed_ensure_ids": "",
|
||||
"failed_ensure_import_refs": "",
|
||||
"failed_set_primary_photos": "",
|
||||
"failed_zero_datetimes": ""
|
||||
}
|
||||
"reports_sub": "Genera diversi report per il tuo inventario."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,31 +2,38 @@
|
||||
"components": {
|
||||
"app": {
|
||||
"create_modal": {
|
||||
"createAndAddAnother": "",
|
||||
"enter": "",
|
||||
"shift": ""
|
||||
"createAndAddAnother": "{shiftKey} + {enterKey}を使用すると、別のアイテムをそのまま追加できます。",
|
||||
"enter": "入る",
|
||||
"shift": "シフト"
|
||||
},
|
||||
"import_dialog": {
|
||||
"change_warning": "【注意】インポート時の動作が変更されました'<br>'\n選択されたCSVファイルにimport_refsの値が存在する場合、該当するアイテムはCSVファイルの値で上書きされます。",
|
||||
"description": "アイテム、ラベル、ロケーション情報を含む CSV ファイルをインポートします。\nデータの形式など詳細については、ドキュメントを参照してください。",
|
||||
"title": ".csvファイルのインポート",
|
||||
"toast": {
|
||||
"import_failed": "",
|
||||
"import_success": "",
|
||||
"please_select_file": ""
|
||||
"import_failed": "読み込みに失敗しました。もう一度お試しください。",
|
||||
"import_success": "インポートに成功しました",
|
||||
"please_select_file": "ファイルを選択してください"
|
||||
}
|
||||
},
|
||||
"outdated": {
|
||||
"current_version": "現在のバージョン",
|
||||
"dismiss": "",
|
||||
"dismiss": "却下",
|
||||
"latest_version": "最新バージョン",
|
||||
"new_version_available": "利用可能な更新があります",
|
||||
"new_version_available_link": "クリックしてリリースノートを表示"
|
||||
}
|
||||
},
|
||||
"color_selector": {
|
||||
"clear": "色をリセット",
|
||||
"color": "色",
|
||||
"no_color": "色設定なし",
|
||||
"no_color_selected": "色を選択していません",
|
||||
"randomize": "色をランダムに変更する"
|
||||
},
|
||||
"form": {
|
||||
"password": {
|
||||
"toggle_show": ""
|
||||
"toggle_show": "パスワードの表示を切り替え"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
@@ -41,7 +48,7 @@
|
||||
"days": "日",
|
||||
"hour": "時間",
|
||||
"hours": "時間",
|
||||
"in": "",
|
||||
"in": "{0}内",
|
||||
"just-now": "たった今",
|
||||
"last-month": "先月",
|
||||
"last-week": "先週",
|
||||
@@ -65,17 +72,17 @@
|
||||
"confirm_description": "この管理ラベルを印刷しますか?",
|
||||
"download": "画像として保存",
|
||||
"print": "プリンターで印刷",
|
||||
"server_print": "",
|
||||
"server_print": "サーバーで印刷",
|
||||
"titles": "管理ラベルの出力",
|
||||
"toast": {
|
||||
"load_status_failed": "",
|
||||
"print_failed": "",
|
||||
"print_success": ""
|
||||
"load_status_failed": "ステータスの読み込みに失敗しました",
|
||||
"print_failed": "ラベルの印刷に失敗しました",
|
||||
"print_success": "ラベルを印刷しました"
|
||||
}
|
||||
},
|
||||
"page_qr_code": {
|
||||
"page_url": "ページ URL",
|
||||
"qr_tooltip": ""
|
||||
"qr_tooltip": "QRコードを表示"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "パスワード強度"
|
||||
@@ -83,40 +90,51 @@
|
||||
},
|
||||
"item": {
|
||||
"attachments_list": {
|
||||
"download": "",
|
||||
"open_new_tab": ""
|
||||
"download": "ダウンロード",
|
||||
"open_new_tab": "新しいタブで開く"
|
||||
},
|
||||
"create_modal": {
|
||||
"delete_photo": "",
|
||||
"delete_photo": "写真を削除",
|
||||
"item_description": "説明",
|
||||
"item_name": "名称",
|
||||
"item_photo": "",
|
||||
"item_quantity": "",
|
||||
"parent_item": "",
|
||||
"rotate_photo": "",
|
||||
"set_as_primary_photo": "",
|
||||
"item_photo": "商品写真 📷",
|
||||
"item_quantity": "数量",
|
||||
"parent_item": "関連アイテム",
|
||||
"product_tooltip_input_barcode": "バーコードを手動入力して自動取得を試みる",
|
||||
"product_tooltip_scan_barcode": "バーコードを撮影して自動取得を試みる",
|
||||
"rotate_photo": "写真を回転",
|
||||
"set_as_primary_photo": "{isPrimary, select, true {non} false {} other {}} サムネイルに設定",
|
||||
"title": "アイテム情報の追加",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": "",
|
||||
"failed_load_parent": "",
|
||||
"no_canvas_support": "",
|
||||
"please_select_location": "",
|
||||
"rotate_failed": "",
|
||||
"rotate_process_failed": "",
|
||||
"some_photos_failed": "",
|
||||
"upload_failed": "",
|
||||
"upload_success": "",
|
||||
"uploading_photos": ""
|
||||
"already_creating": "既に同じアイテムがあります",
|
||||
"create_failed": "アイテムを作成できませんでした",
|
||||
"create_success": "アイテムを作成しました",
|
||||
"failed_load_parent": "親アイテムの読み込みに失敗しました。手動で選択してください",
|
||||
"no_canvas_support": "このブラウザはHTML5 canvasをサポートしていません",
|
||||
"please_select_location": "ロケーションを選択",
|
||||
"rotate_failed": "画像の回転ができませんでした。エラー内容: { error }",
|
||||
"rotate_process_failed": "画像回転の処理でエラーが発生しました",
|
||||
"some_photos_failed": "{count, plural, = 0 {アップロード可能な画像がありません} = 1 {画像のアップロードに失敗しました} other {一部の画像のアップロードに失敗しました}}",
|
||||
"upload_failed": "画像のアップロードに失敗しました ({ photoName })",
|
||||
"upload_success": "{count, plural, = 0 {画像はアップロードされていません} = 1 {画像をアップロードしました} other {すべての画像をアップロードしました}}",
|
||||
"uploading_photos": "{count, plural, = 0 {アップロード可能な画像がありません} = 1 {画像をアップロードしています} other {{count}枚の画像をアップロードしています…}}"
|
||||
},
|
||||
"upload_photos": "",
|
||||
"uploaded": ""
|
||||
"upload_photos": "画像をアップロード",
|
||||
"uploaded": "アップロードした画像"
|
||||
},
|
||||
"product_import": {
|
||||
"barcode": "商品のバーコード",
|
||||
"db_source": "情報提供元",
|
||||
"error_exception": "バーコード情報を取得できませんでした ",
|
||||
"error_invalid_barcode": "このバーコードは使えません",
|
||||
"error_not_found": "入力されたバーコードの製品が見つかりません。手動登録が必要です。",
|
||||
"search_item": "商品を検索",
|
||||
"title": "商品をインポート"
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "",
|
||||
"placeholder": "",
|
||||
"search_placeholder": ""
|
||||
"no_results": "一致するものがありません",
|
||||
"placeholder": "選択してください…",
|
||||
"search_placeholder": "入力してください"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
@@ -126,28 +144,29 @@
|
||||
"table": "テーブル"
|
||||
},
|
||||
"table": {
|
||||
"headers": "",
|
||||
"headers": "ヘッダー",
|
||||
"page": "ページ",
|
||||
"rows_per_page": "表示件数",
|
||||
"table_settings": "",
|
||||
"view_item": ""
|
||||
"table_settings": "テーブル表示の設定",
|
||||
"view_item": "アイテムを見る"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"label_color": "ラベルの色",
|
||||
"label_description": "ラベルの説明",
|
||||
"label_name": "ラベル名",
|
||||
"title": "ラベルの追加",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": "",
|
||||
"label_name_too_long": ""
|
||||
"already_creating": "既に同じラベルがあります",
|
||||
"create_failed": "ラベルを作成できませんでした",
|
||||
"create_success": "ラベルを作成しました",
|
||||
"label_name_too_long": "ラベル名は50文字以内で入力してください"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"select_labels": ""
|
||||
"select_labels": "ラベルを選択"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
@@ -156,69 +175,72 @@
|
||||
"location_name": "名称",
|
||||
"title": "ロケーション(場所)の追加",
|
||||
"toast": {
|
||||
"already_creating": "",
|
||||
"create_failed": "",
|
||||
"create_success": ""
|
||||
"already_creating": "既に同じロケーションがあります",
|
||||
"create_failed": "ロケーションを作成できませんでした",
|
||||
"create_success": "ロケーションを作成しました"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"no_location_found": "",
|
||||
"no_location_found": "一致するロケーションがありません",
|
||||
"parent_location": "親項目 (選択された項目の下位にネスト)",
|
||||
"search_location": "",
|
||||
"select_location": ""
|
||||
"search_location": "ロケーションを検索",
|
||||
"select_location": "ロケーションを選択"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "場所の項目は存在しません。\nナビゲーションバー上部の\"Create\"から追加してください。"
|
||||
"no_locations": "ロケーションが設定されていません。\n左上の '<span class=\"link-primary\">'項目の追加'</span>' ボタンでロケーションを追加してください。"
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
"no_results": "",
|
||||
"no_results": "一致する項目はありません",
|
||||
"shortcut_hint": "数字キーを入力することで対応する操作を実行できます"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"api_failure": "サーバーとの通信に失敗しました "
|
||||
},
|
||||
"global": {
|
||||
"add": "追加",
|
||||
"archived": "",
|
||||
"archived": "アーカイブ済み",
|
||||
"build": "ビルド番号: { build }",
|
||||
"cancel": "",
|
||||
"cancel": "キャンセル",
|
||||
"confirm": "確認が必要です",
|
||||
"create": "追加する",
|
||||
"create": "項目の追加",
|
||||
"create_and_add": "続けて追加できます",
|
||||
"create_subitem": "",
|
||||
"created": "",
|
||||
"create_subitem": "サブアイテムを作成",
|
||||
"created": "作成済み",
|
||||
"delete": "削除",
|
||||
"delete_confirm": "",
|
||||
"demo_instance": "",
|
||||
"delete_confirm": "このアイテムを削除しますか? ",
|
||||
"demo_instance": "これはデモ環境です。データは新しいバージョンになると初期化されます。",
|
||||
"details": "製品の情報",
|
||||
"duplicate": "複製",
|
||||
"edit": "編集",
|
||||
"email": "メール",
|
||||
"follow_dev": "開発者をフォローする",
|
||||
"footer": {
|
||||
"api_link": "",
|
||||
"version_link": ""
|
||||
"api_link": "'<a href=\"https://homebox.software/en/api/\" target=\"_blank\">'APIリファレンス(英語)'</a>'",
|
||||
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Version: { version } Build: { build } '</a>'"
|
||||
},
|
||||
"github": "GitHub プロジェクト",
|
||||
"insured": "",
|
||||
"insured": "保険適用",
|
||||
"items": "アイテム",
|
||||
"join_discord": "Discordサーバーへの参加",
|
||||
"labels": "ラベル",
|
||||
"loading": "",
|
||||
"loading": "読込中…",
|
||||
"locations": "ロケーション",
|
||||
"maintenance": "メンテナンス情報",
|
||||
"name": "名前",
|
||||
"navigate": "",
|
||||
"navigate": "移動",
|
||||
"password": "パスワード",
|
||||
"quantity": "数量",
|
||||
"read_docs": "ドキュメントを読む",
|
||||
"return_home": "",
|
||||
"return_home": "トップページに戻る",
|
||||
"save": "保存",
|
||||
"search": "検索",
|
||||
"sign_out": "ログアウト",
|
||||
"submit": "送信",
|
||||
"unknown": "",
|
||||
"unknown": "不明",
|
||||
"update": "アップデート",
|
||||
"updating": "",
|
||||
"updating": "更新中",
|
||||
"value": "値",
|
||||
"version": "現在のバージョン: { version }",
|
||||
"welcome": "{ username } でログイン中"
|
||||
@@ -239,18 +261,18 @@
|
||||
"joining_group": "既存のグループの招待を受け取りました!",
|
||||
"login": "ログイン",
|
||||
"register": "新規登録",
|
||||
"remember_me": "ログインを維持する",
|
||||
"remember_me": "ログインしたままにする",
|
||||
"set_email": "メールアドレスを入力してください",
|
||||
"set_name": "お名前は何ですか?",
|
||||
"set_password": "パスワードを入力してください",
|
||||
"tagline": "Track, Organize, and Manage your Things.",
|
||||
"title": "",
|
||||
"title": "Organize and Tag Your Stuff",
|
||||
"toast": {
|
||||
"invalid_email": "",
|
||||
"invalid_email_password": "",
|
||||
"login_success": "",
|
||||
"problem_registering": "",
|
||||
"user_registered": ""
|
||||
"invalid_email": "無効なメールアドレス",
|
||||
"invalid_email_password": "無効なメールアドレスまたはパスワードです",
|
||||
"login_success": "ログインしました",
|
||||
"problem_registering": "ユーザー情報を登録できませんでした",
|
||||
"user_registered": "ユーザーを登録しました"
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
@@ -258,25 +280,25 @@
|
||||
"advanced": "詳細項目の表示",
|
||||
"archived": "アーカイブ済み",
|
||||
"asset_id": "Asset ID",
|
||||
"associated_with_multiple": "",
|
||||
"associated_with_multiple": "このAsset IDは重複しています",
|
||||
"attachment": "添付ファイル",
|
||||
"attachments": "添付ファイル",
|
||||
"changes_persisted_immediately": "添付ファイルの保存は自動で行われます",
|
||||
"created_at": "作成日",
|
||||
"custom_fields": "カスタム項目",
|
||||
"delete_attachment_confirm": "",
|
||||
"delete_item_confirm": "",
|
||||
"delete_attachment_confirm": "この添付ファイルを削除しますか?",
|
||||
"delete_item_confirm": "このアイテムを削除しますか?",
|
||||
"description": "説明",
|
||||
"details": "この製品の情報",
|
||||
"drag_and_drop": "添付ファイルはドラッグ&ドロップ、もしくはクリックで追加できます",
|
||||
"edit": {
|
||||
"edit_attachment_dialog": {
|
||||
"attachment_title": "",
|
||||
"attachment_type": "",
|
||||
"primary_photo": "",
|
||||
"primary_photo_sub": "",
|
||||
"select_type": "",
|
||||
"title": ""
|
||||
"attachment_title": "添付ファイルのタイトル",
|
||||
"attachment_type": "添付ファイルの種類",
|
||||
"primary_photo": "サムネイルに設定",
|
||||
"primary_photo_sub": "この画像をアイテムサムネイルに設定します。既に画像が設定されている場合は、このチェックボックスを有効にすると設定が上書きされます。",
|
||||
"select_type": "種類を選択する",
|
||||
"title": "添付ファイルの編集"
|
||||
}
|
||||
},
|
||||
"edit_details": "基本的な情報の編集",
|
||||
@@ -285,7 +307,7 @@
|
||||
"first": "最初の",
|
||||
"include_archive": "「現在は使用していない」にチェックを入れた項目を検索に含める",
|
||||
"insured": "保険適用",
|
||||
"invalid_asset_id": "",
|
||||
"invalid_asset_id": "このAsset IDは無効です",
|
||||
"last": "最後",
|
||||
"lifetime_warranty": "無期限保証",
|
||||
"location": "場所",
|
||||
@@ -296,7 +318,7 @@
|
||||
"name": "名称",
|
||||
"negate_labels": "選択されたラベルを除外",
|
||||
"next_page": "次の ページ",
|
||||
"no_attachments": "",
|
||||
"no_attachments": "添付ファイルはありません",
|
||||
"no_results": "一致する項目はありません",
|
||||
"notes": "備考",
|
||||
"only_with_photo": "写真付きのアイテムのみ",
|
||||
@@ -318,44 +340,44 @@
|
||||
"receipts": "レシート(領収書)",
|
||||
"reset_search": "検索条件をクリア",
|
||||
"results": "検索結果: { total } 件",
|
||||
"select_field": "",
|
||||
"select_field": "フィールドを選択",
|
||||
"serial_number": "シリアル番号(S/N)",
|
||||
"show_advanced_view_options": "高度なオプションを表示",
|
||||
"sold_at": "売却日",
|
||||
"sold_details": "売却時の情報",
|
||||
"sold_price": "売却価格",
|
||||
"sold_to": "売却先",
|
||||
"sync_child_locations": "",
|
||||
"sync_child_locations": "ペア設定されたアイテムの場所も同時に変更する",
|
||||
"tip_1": "フィルター条件を複数選択した場合、\nいずれかに該当している項目すべてをフィルター条件に一致しているとみなします。",
|
||||
"tip_2": "Asset IDで検索する場合は「#」を先頭に入力してください (例: '#000-001')",
|
||||
"tip_3": "カスタム値絞り込み(Field Selector)は、\n複数選択された場合にどれかが該当するアイテムすべてを一致しているとみなしします。",
|
||||
"tips": "ヒント",
|
||||
"tips_sub": "検索に関するヒント",
|
||||
"toast": {
|
||||
"asset_not_found": "",
|
||||
"attachment_deleted": "",
|
||||
"attachment_updated": "",
|
||||
"attachment_uploaded": "",
|
||||
"child_items_location_no_longer_synced": "",
|
||||
"child_items_location_synced": "",
|
||||
"child_location_desync": "",
|
||||
"error_loading_parent_data": "",
|
||||
"failed_adjust_quantity": "",
|
||||
"failed_delete_attachment": "",
|
||||
"failed_delete_item": "",
|
||||
"failed_duplicate_item": "",
|
||||
"failed_load_asset": "",
|
||||
"failed_load_item": "",
|
||||
"failed_load_items": "",
|
||||
"failed_save": "",
|
||||
"failed_save_no_location": "",
|
||||
"failed_search_items": "",
|
||||
"failed_update_attachment": "",
|
||||
"failed_upload_attachment": "",
|
||||
"item_deleted": "",
|
||||
"item_saved": "",
|
||||
"quantity_cannot_negative": "",
|
||||
"sync_child_location": ""
|
||||
"asset_not_found": "アイテムがありません",
|
||||
"attachment_deleted": "添付ファイルを削除しました",
|
||||
"attachment_updated": "添付ファイルを更新",
|
||||
"attachment_uploaded": "添付ファイルをアップロードしました",
|
||||
"child_items_location_no_longer_synced": "子アイテムのロケーションはこのアイテムと同期されなくなります。",
|
||||
"child_items_location_synced": "子アイテムのロケーションはこのアイテムと同期されています",
|
||||
"child_location_desync": "ロケーションを変更すると、親アイテムのロケーション同期が解除されます",
|
||||
"error_loading_parent_data": "親アイテムの読み込み中にエラーが発生しました",
|
||||
"failed_adjust_quantity": "数量の変更に失敗しました",
|
||||
"failed_delete_attachment": "添付ファイルの削除に失敗しました",
|
||||
"failed_delete_item": "アイテムの削除に失敗しました",
|
||||
"failed_duplicate_item": "アイテムの複製に失敗しました",
|
||||
"failed_load_asset": "アセットの読み込みに失敗しました",
|
||||
"failed_load_item": "アイテムの読み込みに失敗しました",
|
||||
"failed_load_items": "アイテムの読み込みに失敗しました",
|
||||
"failed_save": "アイテム設定の保存に失敗しました",
|
||||
"failed_save_no_location": "アイテム設定の保存に失敗しました: ロケーションを選択してください",
|
||||
"failed_search_items": "アイテム検索に失敗しました",
|
||||
"failed_update_attachment": "添付ファイルの更新に失敗しました",
|
||||
"failed_upload_attachment": "添付ファイルのアップロードに失敗しました",
|
||||
"item_deleted": "アイテムを削除しました",
|
||||
"item_saved": "アイテム設定を保存しました",
|
||||
"quantity_cannot_negative": "数量はマイナスにできません",
|
||||
"sync_child_location": "ペア設定されたアイテムのロケーションも更新されました。"
|
||||
},
|
||||
"updated_at": "更新日",
|
||||
"warranty": "保証書",
|
||||
@@ -363,18 +385,19 @@
|
||||
"warranty_expires": "保証期間"
|
||||
},
|
||||
"labels": {
|
||||
"label_delete_confirm": "",
|
||||
"label_delete_confirm": "このラベルを削除しますか?元に戻すことはできません。",
|
||||
"no_results": "一致するラベルはありません",
|
||||
"toast": {
|
||||
"failed_delete_label": "",
|
||||
"failed_load_label": "",
|
||||
"failed_update_label": "",
|
||||
"label_deleted": "",
|
||||
"label_updated": ""
|
||||
"failed_delete_label": "ラベルの削除に失敗しました",
|
||||
"failed_load_label": "ラベルの読み込みに失敗しました",
|
||||
"failed_update_label": "ラベルの更新に失敗しました",
|
||||
"label_deleted": "ラベルが削除されました",
|
||||
"label_updated": "ラベル設定が更新されました"
|
||||
},
|
||||
"update_label": "ラベル設定の変更"
|
||||
},
|
||||
"languages": {
|
||||
"bs-BA": "ボスニア語 (Bosnia and Herzegovina)",
|
||||
"ca": "カタルーニャ語 (カタロニア語)",
|
||||
"cs-CZ": "チェコ語",
|
||||
"de": "ドイツ語",
|
||||
@@ -387,9 +410,9 @@
|
||||
"it": "イタリア語",
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "韓国語",
|
||||
"lb-LU": "",
|
||||
"lt-LT": "",
|
||||
"nb-NO": "",
|
||||
"lb-LU": "ルクセンブルク語 (Luxembourg)",
|
||||
"lt-LT": "リトアニア語 (Lithuania)",
|
||||
"nb-NO": "ノルウェー語 (ブークモール)",
|
||||
"nl": "オランダ語",
|
||||
"pl": "ポーランド語",
|
||||
"pt-BR": "ポルトガル語 (ブラジル)",
|
||||
@@ -401,6 +424,7 @@
|
||||
"th-TH": "タイ語",
|
||||
"tr": "トルコ語",
|
||||
"uk-UA": "ウクライナ語",
|
||||
"vi-VN": "ベトナム語",
|
||||
"zh-CN": "中国語 (簡体字)",
|
||||
"zh-HK": "中国語 (香港)",
|
||||
"zh-MO": "中国語 (マカオ)",
|
||||
@@ -413,15 +437,15 @@
|
||||
"locations": {
|
||||
"child_locations": "属しているその他のロケーション",
|
||||
"collapse_tree": "ツリーを折りたたむ",
|
||||
"expand_tree": "",
|
||||
"location_items_delete_confirm": "",
|
||||
"no_results": "指定された場所は見つかりません",
|
||||
"expand_tree": "ツリーを展開",
|
||||
"location_items_delete_confirm": "このロケーションと、紐づいているアイテムをすべて削除します。'<br>'このロケーションに紐づけられたアイテムはすべて削除されます。'<br>'元に戻すことはできません。",
|
||||
"no_results": "ロケーションはありません",
|
||||
"toast": {
|
||||
"failed_delete_location": "",
|
||||
"failed_load_location": "",
|
||||
"failed_update_location": "",
|
||||
"location_deleted": "",
|
||||
"location_updated": ""
|
||||
"failed_delete_location": "ロケーションの削除に失敗しました",
|
||||
"failed_load_location": "ロケーションの読み込みに失敗しました",
|
||||
"failed_update_location": "ロケーション設定の更新に失敗しました",
|
||||
"location_deleted": "ロケーションを削除しました",
|
||||
"location_updated": "ロケーション設定を更新しました"
|
||||
},
|
||||
"update_location": "場所情報の変更"
|
||||
},
|
||||
@@ -478,10 +502,10 @@
|
||||
"currency_format": "通貨の種類",
|
||||
"current_password": "現在のパスワード",
|
||||
"delete_account": "アカウントの削除 (永久的です!)",
|
||||
"delete_account_confirm": "",
|
||||
"delete_account_confirm": "アカウントを削除しますか?あなたがこのグループのメンバーの最後の一人の場合、アイテム・ロケーション・ラベルなどすべてのデータが削除されます。'<br>'【最終確認】'<u>'この削除機能に再確認ポップアップはありません。続行した場合、すぐにデータが削除され復元もできません。'<u>'",
|
||||
"delete_account_sub": "アカウントと関連するデータをすべて削除します。元に戻すことはできません。",
|
||||
"delete_notifier_confirm": "",
|
||||
"display_legacy_header": "",
|
||||
"delete_notifier_confirm": "この通知設定を削除してもよろしいですか?",
|
||||
"display_legacy_header": "{currentValue, select, true {レガシーヘッダーを無効にする} false {レガシーヘッダーを有効にする} other {ヒットしない}}",
|
||||
"enabled": "有効",
|
||||
"example": "例",
|
||||
"gen_invite": "招待リンクの作成",
|
||||
@@ -491,27 +515,27 @@
|
||||
"language": "言語",
|
||||
"new_password": "新しいパスワード",
|
||||
"no_notifiers": "通知機能は設定されていません",
|
||||
"no_override": "",
|
||||
"notifier_modal": "",
|
||||
"no_override": "既定の言語",
|
||||
"notifier_modal": "{type, select, true {編集} false {作成} other {その他}}通知者",
|
||||
"notifiers": "通知",
|
||||
"notifiers_sub": "メンテナンスなどのリマインダー通知を受け取れます",
|
||||
"override_locale": "",
|
||||
"override_locale": "日時・通貨の言語を設定 (上記の言語設定とは別の言語を設定可能)",
|
||||
"test": "テスト",
|
||||
"theme_settings": "テーマ設定",
|
||||
"theme_settings_sub": "テーマ設定はブラウザに保存されます。いつでも変更できます。\nテーマ設定によって問題が発生した場合は、再読み込みを行ってください。",
|
||||
"toast": {
|
||||
"account_deleted": "",
|
||||
"failed_change_password": "",
|
||||
"failed_create_notifier": "",
|
||||
"failed_delete_account": "",
|
||||
"failed_delete_notifier": "",
|
||||
"failed_get_currencies": "",
|
||||
"failed_test_notifier": "",
|
||||
"failed_update_group": "",
|
||||
"failed_update_notifier": "",
|
||||
"group_updated": "",
|
||||
"notifier_test_success": "",
|
||||
"password_changed": ""
|
||||
"account_deleted": "アカウントを削除しました",
|
||||
"failed_change_password": "パスワードを変更できませんでした",
|
||||
"failed_create_notifier": "通知設定の作成に失敗しました。",
|
||||
"failed_delete_account": "アカウントの削除に失敗しました",
|
||||
"failed_delete_notifier": "通知設定の削除に失敗しました",
|
||||
"failed_get_currencies": "通貨情報の取得に失敗しました",
|
||||
"failed_test_notifier": "通知の送信テストに失敗しました",
|
||||
"failed_update_group": "グループ設定の更新に失敗しました",
|
||||
"failed_update_notifier": "通知設定の更新に失敗しました",
|
||||
"group_updated": "グループ設定を更新しました",
|
||||
"notifier_test_success": "通知の送信テストに成功しました",
|
||||
"password_changed": "パスワードを変更しました"
|
||||
},
|
||||
"update_group": "グループ設定を更新",
|
||||
"update_language": "言語更新",
|
||||
@@ -521,61 +545,67 @@
|
||||
},
|
||||
"reports": {
|
||||
"label_generator": {
|
||||
"asset_end": "",
|
||||
"asset_start": "",
|
||||
"base_url": "",
|
||||
"bordered_labels": "",
|
||||
"generate_page": "",
|
||||
"input_placeholder": "",
|
||||
"instruction_1": "",
|
||||
"instruction_2": "",
|
||||
"instruction_3": "",
|
||||
"label_height": "",
|
||||
"label_width": "",
|
||||
"measure_type": "",
|
||||
"page_bottom_padding": "",
|
||||
"page_height": "",
|
||||
"page_left_padding": "",
|
||||
"page_right_padding": "",
|
||||
"page_top_padding": "",
|
||||
"page_width": "",
|
||||
"qr_code_example": "",
|
||||
"tip_1": "",
|
||||
"tip_2": "",
|
||||
"tip_3": "",
|
||||
"tips": "",
|
||||
"title": "",
|
||||
"asset_end": "Asset ID 印刷終了位置 (このIDは含みません)",
|
||||
"asset_start": "Asset ID 印刷開始位置 (このIDを含みます)",
|
||||
"base_url": "QRコードのURL (通常変更する必要はありません。URL見本を参照し設定してください。)",
|
||||
"bordered_labels": "ラベルの境界線を印刷",
|
||||
"generate_page": "印刷プレビューを取得",
|
||||
"input_placeholder": "ここに入力",
|
||||
"instruction_1": "Homebox ラベルジェネレーターは、Homeboxに登録されたアイテムの管理シールを印刷するツールです。\nシールはアイテムを登録する前に印刷できるため、事前に大量作成することが可能です!",
|
||||
"instruction_2": "シールには、HomeboxのQRコードやAsset IDを印刷できます。\nAsset IDを無効にしている場合でもこのツールは使用できますが、Asset IDを利用してアイテムページを開く機能などは利用できません。",
|
||||
"instruction_3": "この機能はまだ開発中であり、今後の更新で改修される可能性があります。\nバグ情報や機能提案がある方は、ぜひ'<a href=\"https://github.com/sysadminsmedia/homebox/discussions/53\">'GitHubディスカッションに投稿をお願いします。'</a>'",
|
||||
"label_height": "シール 高さ",
|
||||
"label_width": "シール 横幅",
|
||||
"measure_type": "メジャータイプ",
|
||||
"page_bottom_padding": "用紙 下余白",
|
||||
"page_height": "用紙サイズ 高さ",
|
||||
"page_left_padding": "用紙 左余白",
|
||||
"page_right_padding": "用紙 右余白",
|
||||
"page_top_padding": "用紙 上余白",
|
||||
"page_width": "用紙サイズ 横幅",
|
||||
"qr_code_example": "QRコード URL見本",
|
||||
"tip_1": "この機能の既定値は、'<a href=\"https://www.avery.com/templates/5260\">'Avery 5260 label sheets'</a>'と適合する値になっています。\nそれ以外のシートを使用する場合は、必ず設定を調整する必要があります。",
|
||||
"tip_2": "シールをカスタマイズする場合、寸法はインチ単位で設定してください。\n5260シートも、既定値が寸法と一致しないことがあります。\n'<br>' '<b>'試行錯誤が必要になることを覚悟してください。'<u>'必ず寸法や印刷位置の検証を行ってから、シール用紙に印刷してください。'</u>' '</b>'",
|
||||
"tip_3": "印刷時は、以下のことに気を付けてください\n'<ol><li>'余白を0 または なし に設定する\n'</li><li>'拡大率を100%に設定する\n'</li><li>'両面印刷を無効にする\n'</li><li>'ページを印刷する前に必ずテスト印刷をする\n'</li><li>'たくさんの紙くずが発生することを覚悟する",
|
||||
"tips": "仕様",
|
||||
"title": "管理シール印刷機能",
|
||||
"toast": {
|
||||
"page_too_small_card": ""
|
||||
"page_too_small_card": "印刷可能範囲がシールサイズより小さいため印刷プレビューを生成できません。用紙サイズ・シールサイズ・余白を変更してください。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"barcode_detected_message": "バーコードが見つかりました",
|
||||
"barcode_fetch_data": "製品のデータを取得",
|
||||
"error": "不明なエラーが発生しました。スキャンは利用できません。",
|
||||
"invalid_url": "バーコードが無効です",
|
||||
"no_sources": "",
|
||||
"permission_denied": "",
|
||||
"select_video_source": "",
|
||||
"title": "",
|
||||
"no_sources": "映像デバイスが見つかりません",
|
||||
"permission_denied": "カメラアクセスが拒否されています。ブラウザ設定・スマホ設定で許可してください。",
|
||||
"select_video_source": "映像デバイスを選択",
|
||||
"title": "スキャン",
|
||||
"unsupported": "メディアストリームAPIはHTTPSのみをサポートしています"
|
||||
},
|
||||
"tools": {
|
||||
"actions": "全てのアイテムに対する変更",
|
||||
"actions_set": {
|
||||
"create_missing_thumbnails": "サムネイルを自動設定",
|
||||
"create_missing_thumbnails_button": "サムネイルを作成",
|
||||
"create_missing_thumbnails_confirm": "サムネイルを作成してもよろしいですか?件数によっては時間がかかる場合があり、一時停止することはできません。既存のサムネイルは上書きされません。元に戻すことはできません。",
|
||||
"create_missing_thumbnails_sub": "アップロードされた添付ファイルのサムネイルを自動で作成します。既にサムネイルがある場合はスキップされます。サムネイルが存在しない場合に、自動で作成します。性能やファイル数により、処理が完了するまでに時間がかかる場合があります。",
|
||||
"ensure_ids": "アイテムのAsset IDの確認",
|
||||
"ensure_ids_button": "確認の実行",
|
||||
"ensure_ids_confirm": "",
|
||||
"ensure_ids_confirm": "すべてのアイテムにAsset IDがあることを確認します。この処理には時間がかかる場合があり、この処理によってAsset IDが変更されても元に戻すことはできません。",
|
||||
"ensure_ids_sub": "全てのアイテムに、有効なAsset IDが存在することを確認します。'<br>'もし存在しないアイテムがあった場合は、登録された日付が古い順にAsset IDの登録を行います。",
|
||||
"ensure_import_refs": "Import Refsの確認",
|
||||
"ensure_import_refs_button": "確認の実行",
|
||||
"ensure_import_refs_sub": "全てのアイテムに、有効な import_refに対応する値 が設定されていることを確認します。'<br>' 設定されていない場合は、ランダムな8文字の文字列を設定します。",
|
||||
"set_primary_photo": "アイテムの見出し画像の自動設定",
|
||||
"ensure_import_refs": "import_refsの確認",
|
||||
"ensure_import_refs_button": "確認する",
|
||||
"ensure_import_refs_sub": "全てのアイテムに、有効な import_ref の値 が設定されていることを確認します。'<br>' 設定されていない場合は、ランダムな8文字の文字列を設定します。'<br>'import_refは内部管理用のIDで、ユーザーは見ることができません。",
|
||||
"set_primary_photo": "アイテムサムネイルの自動設定",
|
||||
"set_primary_photo_button": "操作を実行",
|
||||
"set_primary_photo_confirm": "",
|
||||
"set_primary_photo_sub": "Homebox v0.10.0にて、画像の添付ファイルのうち1枚を見出し画像(Primary Photo)として設定できるようになりました。'<br>'未設定のアイテムは、この機能を利用して自動で設定できます。'<br>'この機能は、見出し画像が未設定のアイテムと関連付けられている中で最も古い画像ファイルを自動的にアイテムの見出し画像として設定します。'<br>'詳しくは、'<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'このページをご覧ください (GitHub PR #576)'</a>'",
|
||||
"set_primary_photo_confirm": "サムネイルを自動設定します。'<br>'件数によっては時間がかかる場合があり、元に戻すことはできません。",
|
||||
"set_primary_photo_sub": "Homebox v0.10.0 (2023年10月10日リリース)にて、アイテムのサムネイル画像を設定できるようになりました。'<br>'サムネイルが設定されていないアイテムは、この機能を利用して自動で設定できます。'<br>'この機能は、サムネイルが未設定のアイテムの添付画像の中で最も古い画像ファイルを自動的にサムネイルとして設定します。'<br>'詳しくは、'<a class=\"link\" target=\"_brank\" href=\"https://github.com/hay-kot/homebox/pull/576\">'このページをご覧ください (GitHub PR #576)'</a>'",
|
||||
"zero_datetimes": "アイテムに設定された日時を消去",
|
||||
"zero_datetimes_button": "操作を実行",
|
||||
"zero_datetimes_confirm": "",
|
||||
"zero_datetimes_confirm": "すべてアイテムの日付と時刻の値をリセットします。'<br>'件数によっては時間がかかる場合があり、元に戻すことはできません。",
|
||||
"zero_datetimes_sub": "全てのアイテムの'<b>'日付をリセット'</b>'します。'<br>'これは、初期(v0.8.0 / 2023-02-18以前)に発生したバグの修正に必要です。日付が適切に表示されなくなるといった問題が発生している場合に限り、実行してください。'<br>'バグが発生していない場合は実行は不要です。'<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'詳しくはこちらをご覧ください (GitHub Issue #236)'</a>'"
|
||||
},
|
||||
"actions_sub": "以下の機能は、すべてのアイテムのデータに影響を与えます。'<b>'これらの操作を元に戻すことはできません。'</b>'",
|
||||
@@ -586,8 +616,8 @@
|
||||
"export_sub": "Homeboxで利用可能なCSV形式のファイルをエクスポートします。'<br>'すべてのアイテムが対象です。一部のアイテムを選択してエクスポートすることはできません。",
|
||||
"import": "インポート (CSVの取り込み)",
|
||||
"import_button": "CSVファイルを選択",
|
||||
"import_ref_confirm": "",
|
||||
"import_sub": "Homeboxで利用可能なCSVファイルをインポートします。'<br>''<code>'HB.import_ref'</code>'が存在しないアイテムに重複がある場合は上書きされません。'<br>''<code>'HB.import_ref'</code>'が存在するアイテムに重複がある場合は、同じ'<code>'HB.import_ref'</code>'を持つアイテムのデータが上書きされます。'<br>'どちらも、重複していない場合は関係なく追加されます。"
|
||||
"import_ref_confirm": "すべてのアイテムに import_ref の値があることを確認します。'<br>'件数によっては時間がかかる場合があり、元に戻すことはできません。",
|
||||
"import_sub": "Homeboxで利用可能なCSVファイルをインポートします。\n'<code>'HB.import_ref'</code>'が存在しないアイテムに重複がある場合は上書きされません。\n'<code>'HB.import_ref'</code>'が存在するアイテムに重複がある場合は、同じ'<code>'HB.import_ref'</code>'を持つアイテムのデータが上書きされます。\nどちらも、重複していない場合は関係なく追加されます。"
|
||||
},
|
||||
"import_export_sub": "登録されたアイテムをCSVファイルにインポートおよびエクスポートします。Homeboxのソフトウェアを切り替える場合などに便利です。",
|
||||
"reports": "レポート",
|
||||
@@ -601,11 +631,12 @@
|
||||
},
|
||||
"reports_sub": "アイテムに関するデータの含まれたファイルを作成できます。",
|
||||
"toast": {
|
||||
"asset_success": "",
|
||||
"failed_ensure_ids": "",
|
||||
"failed_ensure_import_refs": "",
|
||||
"failed_set_primary_photos": "",
|
||||
"failed_zero_datetimes": ""
|
||||
"asset_success": "{results}件のアイテムが更新されました。",
|
||||
"failed_create_missing_thumbnails": "サムネイルの自動作成ができませんでした。",
|
||||
"failed_ensure_ids": "Asset IDの確認に失敗しました。",
|
||||
"failed_ensure_import_refs": "import_ref の確認に失敗しました。",
|
||||
"failed_set_primary_photos": "サムネイルの設定に失敗しました。",
|
||||
"failed_zero_datetimes": "日時のリセットに失敗しました。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user