Compare commits

...
Author SHA1 Message Date
Releases Homarr 168b587c18 chore(release): 1.0.0-beta.10 [skip ci] 2024-12-31 10:42:11 +00:00
homarr-releases[bot] dc12a82805 chore(release): automatic release v1.0.0 2024-12-31 10:41:19 +00:00
ManuelandMeier Lukas f5076454cd feat(integration): add search engine creation (#1816)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-31 11:40:37 +01:00
Meier Lukas aeb681a858 feat(docker): add support for pgid and puid env variables (#1759) 2024-12-31 11:36:28 +01:00
Meier Lukas 56b57ad171 fix(docker): replace anonymous docker volume with env variable for encrypting secrets (#1809) 2024-12-31 11:30:29 +01:00
Manuel 91e1cef611 feat: add request media (#1811) 2024-12-31 11:05:30 +01:00
homarr-renovate[bot] e53b06e8c2 chore(deps): update dependency concurrently to ^9.1.2 (#1818)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-31 08:46:41 +00:00
Meier Lukas 0ebf4bc55e feat(widget): add minecraft server status widget (#1801) 2024-12-31 09:45:48 +01:00
homarr-crowdin[bot] b8a155dd08 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-31 00:43:59 +00:00
Thomas CamlongandMeier Lukas 63786eed06 chore(lang): updated translations from crowdin (#1803)
* New translations en.json (Chinese Simplified)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Croatian)

* New translations en.json (Estonian)

* New translations en.json (Latvian)

* New translations en.json (Cree)

* New translations en.json (Hebrew)

* New translations en.json (Hebrew)

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-30 21:44:23 +01:00
homarr-renovate[bot] 598f0047eb fix(deps): update dependency typescript-eslint to ^8.19.0 (#1814)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-30 17:33:38 +00:00
homarr-renovate[bot] eacda4c397 fix(deps): update tiptap monorepo to v2.11.0 (#1813)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-30 16:54:00 +00:00
Meier Lukas 5cb726f793 ci(docker): always push image (#1812) 2024-12-30 17:38:27 +01:00
homarr-crowdin[bot] e8d29420e0 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-30 16:28:16 +00:00
Yossi Hillali 23c7d0bbf0 fix(media-server): redesign widget (#1775)
* fix: redisign media server widget

* fix: reviewed changes

* fix: reviewed chenges

* fix: add icon title

* fix: text resize
2024-12-29 19:24:38 +02:00
homarr-renovate[bot] 75aefcb0d5 fix(deps): update dependency octokit to ^4.0.3 (#1806)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-29 16:16:34 +00:00
homarr-crowdin[bot] b52a8a7935 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-29 08:27:17 +00:00
homarr-renovate[bot] 6d90209e00 chore(deps): update pnpm to v9.15.2 (#1804)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-29 08:26:48 +00:00
Thomas CamlongandMeier Lukas 8f0945f51a chore(lang): updated translations from crowdin (#1800)
* New translations en.json (German)

* New translations en.json (Hebrew)

* New translations en.json (Hebrew)

* New translations en.json (Turkish)

* New translations en.json (Chinese Simplified)

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-28 18:50:25 +01:00
homarr-crowdin[bot] 5b04b93f23 chore(lang): updated translations from crowdin (#1802)
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-28 18:49:22 +01:00
Thomas CamlongandMeier Lukas c8228b920c chore(lang): updated translations from crowdin (#1797)
* New translations en.json (Hebrew)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-28 10:25:14 +01:00
homarr-crowdin[bot] abf7942fb7 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-28 09:14:42 +00:00
homarr-crowdin[bot] 50746eb3e4 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-28 09:06:24 +00:00
Thomas CamlongandMeier Lukas 76abfca11b chore(lang): updated translations from crowdin (#1786)
* New translations en.json (Hebrew)

* New translations en.json (Dutch)

* New translations en.json (Hebrew)

* New translations en.json (Hebrew)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Croatian)

* New translations en.json (Latvian)

* New translations en.json (Cree)

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-27 20:47:14 +01:00
homarr-crowdin[bot] fcab6a347d chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-27 19:43:44 +00:00
Yossi Hillali c91aae53ac fix(health-monitoring): add months to uptime, fix status update (#1792) 2024-12-27 21:32:04 +02:00
homarr-renovate[bot]andMeier Lukas 4943e7b851 fix(deps): update dependency trpc-to-openapi to ^2.1.1 (#1788)
* fix(deps): update dependency trpc-to-openapi to ^2.1.1

* fix(deps): missing dependency lodash.clonedeep in api package

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-27 20:31:15 +01:00
Releases Homarr 88c423fca6 chore(release): 1.0.0-beta.9 [skip ci] 2024-12-27 19:13:01 +00:00
homarr-releases[bot] f238663ec7 chore(release): automatic release v1.0.0 2024-12-27 19:12:07 +00:00
homarr-renovate[bot] e9870e7ee7 fix(deps): update tanstack-query monorepo to ^5.62.11 (#1791)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-27 13:22:52 +00:00
homarr-renovate[bot] 9c0a0ced04 chore(deps): update dependency concurrently to ^9.1.1 (#1790)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-27 11:53:46 +00:00
homarr-crowdin[bot] 4c27635066 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-27 00:44:11 +00:00
homarr-renovate[bot] 3d66ff9148 fix(deps): update nextjs monorepo to ^14.2.22 (#1787)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-26 18:20:53 +00:00
Meier Lukas ec257e5f60 chore(env): remove unused vercel-url env variable (#1779)
* chore(env): remove unused vercel-url env variable

* chore: remove vercel-json file
2024-12-26 08:58:27 +01:00
Meier Lukas ef24370a8c refactor: revert assignment of oldmarr widget mapping (#1780) 2024-12-26 08:58:06 +01:00
Releases Homarr 1fa0fc50c5 chore(release): 1.0.0-beta.8 [skip ci] 2024-12-25 14:19:38 +00:00
homarr-releases[bot] e9a1dae857 chore(release): automatic release v1.0.0 2024-12-25 14:18:45 +00:00
Meier Lukas e37279fe80 fix: oldmarr import user casing wrong (#1784) 2024-12-25 15:17:51 +01:00
homarr-renovate[bot] 82c6ce7b77 fix(deps): update dependency drizzle-orm to ^0.38.3 (#1783)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-25 12:25:14 +00:00
homarr-renovate[bot] 555212e2dc fix(deps): update dependency drizzle-zod to ^0.6.1 (#1782)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-25 10:53:06 +00:00
homarr-crowdin[bot] c831539772 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-25 00:43:00 +00:00
homarr-renovate[bot] 3e2ffbd7bf fix(deps): update dependency @ctrl/transmission to ^7.2.0 (#1777)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-24 20:33:01 +00:00
Thomas Camlong 22d7f676eb chore(lang): updated translations from crowdin (#1774)
* New translations en.json (Hebrew)

* New translations en.json (Hebrew)
2024-12-24 21:26:29 +01:00
homarr-crowdin[bot] 23bf262f4e chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-24 20:26:03 +00:00
homarr-renovate[bot] ecae80d741 fix(deps): update dependency @ctrl/deluge to ^7.1.0 (#1776)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-24 20:16:58 +00:00
113 changed files with 7194 additions and 2809 deletions

No files matched your search

+8 -5
View File
@@ -4,6 +4,14 @@
# This file will be committed to version control, so make sure not to have any secrets in it.
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
# The below secret is not used anywhere but required for Auth.js (Would encrypt JWTs and Mail hashes, both not used)
AUTH_SECRET="supersecret"
# The below secret is used to encrypt integration secrets in the database.
# It should be a 32-byte string, generated by running `openssl rand -hex 32` on Unix
# or starting the project without any (which will show a randomly generated one).
SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
# This is how you can use the sqlite driver:
DB_DRIVER='better-sqlite3'
DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
@@ -20,11 +28,6 @@ DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
# DB_PASSWORD='password'
# DB_NAME='name-of-database'
# You can generate the secret via 'openssl rand -base64 32' on Unix
# @see https://next-auth.js.org/configuration/options#secret
AUTH_SECRET='supersecret'
TURBO_TELEMETRY_DISABLED=1
# Configure logging to use winston logger
@@ -13,11 +13,6 @@ on:
required: false
default: true
description: Send notifications
push-image:
type: boolean
required: false
default: true
description: Push Docker Image
permissions:
contents: write
@@ -112,7 +107,6 @@ jobs:
NEXT_VERSION: ${{ needs.release.outputs.version }}
DEPLOY_LATEST: ${{ github.ref_name == 'main' }}
DEPLOY_BETA: ${{ github.ref_name == 'beta' }}
PUSH_IMAGE: ${{ github.event_name != 'workflow_dispatch' || github.events.inputs.push-image == true }}
steps:
- uses: actions/checkout@v4
with:
@@ -143,13 +137,13 @@ jobs:
${{ env.DEPLOY_LATEST == 'true' && 'type=raw,value=latest' || null }}
${{ env.DEPLOY_BETA == 'true' && 'type=raw,value=beta' || null }}
type=raw,value=${{ env.NEXT_VERSION }}
- name: Build and maybe push
- name: Build and push
id: buildPushAction
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
context: .
push: ${{ env.PUSH_IMAGE }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
network: host
@@ -160,4 +154,4 @@ jobs:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: "Deployment of image has completed for branch ${{ github.ref_name }}. Image ID is '${{ steps.buildPushAction.outputs.imageid }}'. ${{ env.PUSH_IMAGE == 'true' && '' || 'This was a dry run' }}"
args: "Deployment of image has completed for branch ${{ github.ref_name }}. Image ID is '${{ steps.buildPushAction.outputs.imageid }}'."
+1 -1
View File
@@ -39,5 +39,5 @@
"i18n-ally.localesPaths": [
"packages/translation/src/lang",
],
"i18n-ally.keystyle": "auto",
"i18n-ally.keystyle": "nested",
}
+36
View File
@@ -1,3 +1,39 @@
## [1.0.0-beta.10](https://github.com/homarr-labs/homarr/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2024-12-31)
### Features
* add request media ([#1811](https://github.com/homarr-labs/homarr/issues/1811)) ([91e1cef](https://github.com/homarr-labs/homarr/commit/91e1cef611a47d57b42c91c0c6d49fcf2e6a10bb))
* **docker:** add support for pgid and puid env variables ([#1759](https://github.com/homarr-labs/homarr/issues/1759)) ([aeb681a](https://github.com/homarr-labs/homarr/commit/aeb681a85839e31284e8f1566adc4191a68ea385))
* **integration:** add search engine creation ([#1816](https://github.com/homarr-labs/homarr/issues/1816)) ([f507645](https://github.com/homarr-labs/homarr/commit/f5076454cd644a99439b697f24bb0b1f871b2205))
* **widget:** add minecraft server status widget ([#1801](https://github.com/homarr-labs/homarr/issues/1801)) ([0ebf4bc](https://github.com/homarr-labs/homarr/commit/0ebf4bc55e62caf5917340496abb350d564df255))
### Bug Fixes
* **deps:** update dependency octokit to ^4.0.3 ([#1806](https://github.com/homarr-labs/homarr/issues/1806)) ([75aefcb](https://github.com/homarr-labs/homarr/commit/75aefcb0d5210a53e8a00b675dbbce847aa8add4))
* **deps:** update dependency trpc-to-openapi to ^2.1.1 ([#1788](https://github.com/homarr-labs/homarr/issues/1788)) ([4943e7b](https://github.com/homarr-labs/homarr/commit/4943e7b851cd29291b5e8f2f9d0e084b4deeffc0))
* **deps:** update dependency typescript-eslint to ^8.19.0 ([#1814](https://github.com/homarr-labs/homarr/issues/1814)) ([598f004](https://github.com/homarr-labs/homarr/commit/598f0047eb13d6d0d6b68c9032a7a6648f4b6aa3))
* **deps:** update tiptap monorepo to v2.11.0 ([#1813](https://github.com/homarr-labs/homarr/issues/1813)) ([eacda4c](https://github.com/homarr-labs/homarr/commit/eacda4c397cb384a35e9eec0305f9b26f2a22a4a))
* **docker:** replace anonymous docker volume with env variable for encrypting secrets ([#1809](https://github.com/homarr-labs/homarr/issues/1809)) ([56b57ad](https://github.com/homarr-labs/homarr/commit/56b57ad171da170c2d8edd6112092cef0b586852))
* **health-monitoring:** add months to uptime, fix status update ([#1792](https://github.com/homarr-labs/homarr/issues/1792)) ([c91aae5](https://github.com/homarr-labs/homarr/commit/c91aae53acf27eab96a9150281f98a2156e9e2af))
* **media-server:** redesign widget ([#1775](https://github.com/homarr-labs/homarr/issues/1775)) ([23c7d0b](https://github.com/homarr-labs/homarr/commit/23c7d0bbf01215b680f346e7eef8072d8b9db09e))
## [1.0.0-beta.9](https://github.com/homarr-labs/homarr/compare/v1.0.0-beta.8...v1.0.0-beta.9) (2024-12-27)
### Bug Fixes
* **deps:** update nextjs monorepo to ^14.2.22 ([#1787](https://github.com/homarr-labs/homarr/issues/1787)) ([3d66ff9](https://github.com/homarr-labs/homarr/commit/3d66ff914883a665c0329c8fc4fd087f4d220692))
* **deps:** update tanstack-query monorepo to ^5.62.11 ([#1791](https://github.com/homarr-labs/homarr/issues/1791)) ([e9870e7](https://github.com/homarr-labs/homarr/commit/e9870e7ee78227d3be773d9d93aa732760cc8e21))
## [1.0.0-beta.8](https://github.com/homarr-labs/homarr/compare/v1.0.0-beta.7...v1.0.0-beta.8) (2024-12-25)
### Bug Fixes
* **deps:** update dependency @ctrl/deluge to ^7.1.0 ([#1776](https://github.com/homarr-labs/homarr/issues/1776)) ([ecae80d](https://github.com/homarr-labs/homarr/commit/ecae80d741213f12ff08f87992cc4d906cbe0085))
* **deps:** update dependency @ctrl/transmission to ^7.2.0 ([#1777](https://github.com/homarr-labs/homarr/issues/1777)) ([3e2ffbd](https://github.com/homarr-labs/homarr/commit/3e2ffbd7bf2793ff58136ed75b38e0d96b4a5ca8))
* **deps:** update dependency drizzle-orm to ^0.38.3 ([#1783](https://github.com/homarr-labs/homarr/issues/1783)) ([82c6ce7](https://github.com/homarr-labs/homarr/commit/82c6ce7b77023881bd4c74fb43437e777f9b219e))
* **deps:** update dependency drizzle-zod to ^0.6.1 ([#1782](https://github.com/homarr-labs/homarr/issues/1782)) ([555212e](https://github.com/homarr-labs/homarr/commit/555212e2dc82c4b71b8c25a5f20964b3441b5ab9))
* oldmarr import user casing wrong ([#1784](https://github.com/homarr-labs/homarr/issues/1784)) ([e37279f](https://github.com/homarr-labs/homarr/commit/e37279fe8071773f1a9773e3233bc81cbb336798))
## [1.0.0-beta.7](https://github.com/homarr-labs/homarr/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2024-12-24)
### Features
+20 -30
View File
@@ -25,51 +25,41 @@ RUN corepack enable pnpm && pnpm build
FROM base AS runner
WORKDIR /app
# gettext is required for envsubst
RUN apk add --no-cache redis nginx bash gettext su-exec
# gettext is required for envsubst, openssl for generating AUTH_SECRET, su-exec for running application as non-root
RUN apk add --no-cache redis nginx bash gettext su-exec openssl
RUN mkdir /appdata
VOLUME /appdata
RUN mkdir /secrets
VOLUME /secrets
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Enable homarr cli
COPY --from=builder --chown=nextjs:nodejs /app/packages/cli/cli.cjs /app/apps/cli/cli.cjs
COPY --from=builder /app/packages/cli/cli.cjs /app/apps/cli/cli.cjs
RUN echo $'#!/bin/bash\ncd /app/apps/cli && node ./cli.cjs "$@"' > /usr/bin/homarr
RUN chmod +x /usr/bin/homarr
# Don't run production as root
RUN chown -R nextjs:nodejs /secrets
RUN mkdir -p /var/cache/nginx && chown -R nextjs:nodejs /var/cache/nginx && \
mkdir -p /var/log/nginx && chown -R nextjs:nodejs /var/log/nginx && \
mkdir -p /var/lib/nginx && chown -R nextjs:nodejs /var/lib/nginx && \
touch /run/nginx/nginx.pid && chown -R nextjs:nodejs /run/nginx/nginx.pid && \
mkdir -p /etc/nginx/templates /etc/nginx/ssl/certs && chown -R nextjs:nodejs /etc/nginx
RUN mkdir -p /var/cache/nginx && \
mkdir -p /var/log/nginx && \
mkdir -p /var/lib/nginx && \
touch /run/nginx/nginx.pid && \
mkdir -p /etc/nginx/templates /etc/nginx/ssl/certs
COPY --from=builder /app/apps/nextjs/next.config.mjs .
COPY --from=builder /app/apps/nextjs/package.json .
COPY --from=builder --chown=nextjs:nodejs /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
COPY --from=builder --chown=nextjs:nodejs /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
COPY --from=builder /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
COPY --from=builder /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
COPY --from=builder /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
COPY --from=builder --chown=nextjs:nodejs /app/packages/db/migrations ./db/migrations
COPY --from=builder /app/packages/db/migrations ./db/migrations
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/apps/nextjs/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
COPY --from=builder --chown=nextjs:nodejs /app/apps/nextjs/public ./apps/nextjs/public
COPY --chown=nextjs:nodejs scripts/run.sh ./run.sh
COPY scripts/entrypoint.sh ./entrypoint.sh
RUN chmod +x ./entrypoint.sh
COPY --chown=nextjs:nodejs scripts/generateRandomSecureKey.js ./generateRandomSecureKey.js
COPY --chown=nextjs:nodejs packages/redis/redis.conf /app/redis.conf
COPY --chown=nextjs:nodejs nginx.conf /etc/nginx/templates/nginx.conf
COPY --from=builder /app/apps/nextjs/.next/standalone ./
COPY --from=builder /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
COPY --from=builder /app/apps/nextjs/public ./apps/nextjs/public
COPY scripts/run.sh ./run.sh
COPY --chmod=777 scripts/entrypoint.sh ./entrypoint.sh
COPY packages/redis/redis.conf /app/redis.conf
COPY nginx.conf /etc/nginx/templates/nginx.conf
ENV DB_URL='/appdata/db/db.sqlite'
@@ -78,4 +68,4 @@ ENV DB_DRIVER='better-sqlite3'
ENV AUTH_PROVIDERS='credentials'
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD ["sh", "run.sh"]
CMD ["sh", "run.sh"]
+1
View File
@@ -1,6 +1,7 @@
// Importing env files here to validate on build
import "@homarr/auth/env.mjs";
import "@homarr/db/env.mjs";
import "@homarr/common/env.mjs";
import MillionLint from "@million/lint";
import createNextIntlPlugin from "next-intl/plugin";
+5 -5
View File
@@ -47,9 +47,9 @@
"@million/lint": "1.0.14",
"@t3-oss/env-nextjs": "^0.11.1",
"@tabler/icons-react": "^3.26.0",
"@tanstack/react-query": "^5.62.10",
"@tanstack/react-query-devtools": "^5.62.10",
"@tanstack/react-query-next-experimental": "5.62.10",
"@tanstack/react-query": "^5.62.11",
"@tanstack/react-query-devtools": "^5.62.11",
"@tanstack/react-query-next-experimental": "5.62.11",
"@trpc/client": "next",
"@trpc/next": "next",
"@trpc/react-query": "next",
@@ -65,7 +65,7 @@
"glob": "^11.0.0",
"jotai": "^2.11.0",
"mantine-react-table": "2.0.0-beta.7",
"next": "^14.2.21",
"next": "^14.2.22",
"postcss-preset-mantine": "^1.17.0",
"prismjs": "^1.29.0",
"react": "^19.0.0",
@@ -87,7 +87,7 @@
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"@types/swagger-ui-react": "^4.18.3",
"concurrently": "^9.1.0",
"concurrently": "^9.1.2",
"eslint": "^9.17.0",
"node-loader": "^2.1.0",
"prettier": "^3.4.2",
@@ -3,13 +3,13 @@
import { useCallback } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Alert, Button, Fieldset, Group, SegmentedControl, Stack, Text, TextInput } from "@mantine/core";
import { Alert, Button, Checkbox, Fieldset, Group, SegmentedControl, Stack, Text, TextInput } from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
import { getAllSecretKindOptions, getIntegrationName } from "@homarr/definitions";
import { getAllSecretKindOptions, getIntegrationName, integrationDefs } from "@homarr/definitions";
import type { UseFormReturnType } from "@homarr/form";
import { useZodForm } from "@homarr/form";
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
@@ -38,6 +38,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
kind,
value: "",
})),
attemptSearchEngineCreation: true,
},
});
const { mutateAsync, isPending } = clientApi.integration.create.useMutation();
@@ -78,6 +79,8 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
);
};
const supportsSearchEngine = integrationDefs[searchParams.kind].category.flat().includes("search");
return (
<form onSubmit={form.onSubmit((value) => void handleSubmitAsync(value))}>
<Stack>
@@ -104,6 +107,16 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
</Stack>
</Fieldset>
{supportsSearchEngine && (
<Checkbox
label={t("integration.field.attemptSearchEngineCreation.label")}
description={t("integration.field.attemptSearchEngineCreation.description", {
kind: getIntegrationName(searchParams.kind),
})}
{...form.getInputProps("attemptSearchEngineCreation", { type: "checkbox" })}
/>
)}
<Group justify="end" align="center">
<Button variant="default" component={Link} href="/manage/integrations">
{t("common.action.backToOverview")}
-5
View File
@@ -3,10 +3,6 @@ import { z } from "zod";
export const env = createEnv({
shared: {
VERCEL_URL: z
.string()
.optional()
.transform((url) => (url ? `https://${url}` : undefined)),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
},
@@ -30,7 +26,6 @@ export const env = createEnv({
* Destructure all variables from `process.env` to make sure they aren't tree-shaken away.
*/
runtimeEnv: {
VERCEL_URL: process.env.VERCEL_URL,
PORT: process.env.PORT,
NODE_ENV: process.env.NODE_ENV,
DOCKER_HOSTNAMES: process.env.DOCKER_HOSTNAMES,
+18 -3
View File
@@ -5,7 +5,22 @@ export const createHomarrContainer = () => {
throw new Error("This test should only be run in CI or with a homarr image named 'homarr-e2e'");
}
return new GenericContainer("homarr-e2e")
.withExposedPorts(7575)
.withWaitStrategy(Wait.forHttp("/api/health/ready", 7575));
return withLogs(
new GenericContainer("homarr-e2e")
.withExposedPorts(7575)
.withEnvironment({
SECRET_ENCRYPTION_KEY: "0".repeat(64),
})
.withWaitStrategy(Wait.forHttp("/api/health/ready", 7575)),
);
};
export const withLogs = (container: GenericContainer) => {
container.withLogConsumer((stream) =>
stream
.on("data", (line) => console.log(line))
.on("err", (line) => console.error(line))
.on("end", () => console.log("Stream closed")),
);
return container;
};
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.0.0-beta.7",
"version": "1.0.0-beta.10",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -52,7 +52,7 @@
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^2.1.8"
},
"packageManager": "pnpm@9.15.1",
"packageManager": "pnpm@9.15.2",
"engines": {
"node": ">=22.12.0"
},
+3 -2
View File
@@ -41,10 +41,11 @@
"@trpc/react-query": "next",
"@trpc/server": "next",
"dockerode": "^4.0.2",
"next": "^14.2.21",
"lodash.clonedeep": "^4.5.0",
"next": "^14.2.22",
"react": "^19.0.0",
"superjson": "2.2.2",
"trpc-to-openapi": "^2.1.0"
"trpc-to-openapi": "^2.1.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
-1
View File
@@ -16,7 +16,6 @@ export const fetchApi = createTRPCClient<AppRouter>({
function getBaseUrl() {
if (typeof window !== "undefined") return window.location.origin;
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return `http://localhost:${process.env.PORT ?? 3000}`;
}
@@ -11,9 +11,11 @@ import {
integrations,
integrationSecrets,
integrationUserPermissions,
searchEngines,
} from "@homarr/db/schema";
import type { IntegrationSecretKind } from "@homarr/definitions";
import {
getIconUrl,
getIntegrationKindsByCategory,
getPermissionsWithParents,
integrationDefs,
@@ -192,6 +194,18 @@ export const integrationRouter = createTRPCRouter({
})),
);
}
if (input.attemptSearchEngineCreation) {
const icon = getIconUrl(input.kind);
await ctx.db.insert(searchEngines).values({
id: createId(),
name: input.name,
integrationId,
type: "fromIntegration",
iconUrl: icon,
short: await getNextValidShortNameForSearchEngineAsync(ctx.db, input.name),
});
}
}),
update: protectedProcedure.input(validation.integration.update).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
@@ -411,6 +425,36 @@ interface AddSecretInput {
value: string;
kind: IntegrationSecretKind;
}
const getNextValidShortNameForSearchEngineAsync = async (db: Database, integrationName: string) => {
const searchEngines = await db.query.searchEngines.findMany({
columns: {
short: true,
},
});
const usedShortNames = searchEngines.flatMap((searchEngine) => searchEngine.short.toLowerCase());
const nameByIntegrationName = integrationName.slice(0, 1).toLowerCase();
if (!usedShortNames.includes(nameByIntegrationName)) {
return nameByIntegrationName;
}
// 8 is max length constraint
for (let i = 2; i < 9999999; i++) {
const generatedName = `${nameByIntegrationName}${i}`;
if (usedShortNames.includes(generatedName)) {
continue;
}
return generatedName;
}
throw new Error(
"Unable to automatically generate a short name. All possible variations were exhausted. Please disable the automatic creation and choose one later yourself.",
);
};
const addSecretAsync = async (db: Database, input: AddSecretInput) => {
await db.insert(integrationSecrets).values({
kind: input.kind,
@@ -2,8 +2,10 @@ import { TRPCError } from "@trpc/server";
import { createId, eq, like, sql } from "@homarr/db";
import { searchEngines } from "@homarr/db/schema";
import { integrationCreator } from "@homarr/integrations";
import { validation } from "@homarr/validation";
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../../trpc";
export const searchEngineRouter = createTRPCRouter({
@@ -56,9 +58,32 @@ export const searchEngineRouter = createTRPCRouter({
search: protectedProcedure.input(validation.common.search).query(async ({ ctx, input }) => {
return await ctx.db.query.searchEngines.findMany({
where: like(searchEngines.short, `${input.query.toLowerCase().trim()}%`),
with: {
integration: {
columns: {
kind: true,
url: true,
id: true,
},
},
},
limit: input.limit,
});
}),
getMediaRequestOptions: protectedProcedure
.unstable_concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr"))
.input(validation.common.mediaRequestOptions)
.query(async ({ ctx, input }) => {
const integration = integrationCreator(ctx.integration);
return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
}),
requestMedia: protectedProcedure
.unstable_concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr"))
.input(validation.common.requestMedia)
.mutation(async ({ ctx, input }) => {
const integration = integrationCreator(ctx.integration);
return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons);
}),
create: permissionRequiredProcedure
.requiresPermission("search-engine-create")
.input(validation.searchEngine.manage)
@@ -179,6 +179,7 @@ describe("create should create a new integration", () => {
kind: "jellyfin" as const,
url: "http://jellyfin.local",
secrets: [{ kind: "apiKey" as const, value: "1234567890" }],
attemptSearchEngineCreation: false,
};
const fakeNow = new Date("2023-07-01T00:00:00Z");
@@ -201,6 +202,48 @@ describe("create should create a new integration", () => {
expect(dbSecret!.updatedAt).toEqual(fakeNow);
});
test("with create integration access should create a new integration when creating search engine", async () => {
const db = createDb();
const caller = integrationRouter.createCaller({
db,
session: defaultSessionWithPermissions(["integration-create"]),
});
const input = {
name: "Jellyseerr",
kind: "jellyseerr" as const,
url: "http://jellyseerr.local",
secrets: [{ kind: "apiKey" as const, value: "1234567890" }],
attemptSearchEngineCreation: true,
};
const fakeNow = new Date("2023-07-01T00:00:00Z");
vi.useFakeTimers();
vi.setSystemTime(fakeNow);
await caller.create(input);
vi.useRealTimers();
const dbIntegration = await db.query.integrations.findFirst();
const dbSecret = await db.query.integrationSecrets.findFirst();
const dbSearchEngine = await db.query.searchEngines.findFirst();
expect(dbIntegration).toBeDefined();
expect(dbIntegration!.name).toBe(input.name);
expect(dbIntegration!.kind).toBe(input.kind);
expect(dbIntegration!.url).toBe(input.url);
expect(dbSecret!.integrationId).toBe(dbIntegration!.id);
expect(dbSecret).toBeDefined();
expect(dbSecret!.kind).toBe(input.secrets[0]!.kind);
expect(dbSecret!.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/);
expect(dbSecret!.updatedAt).toEqual(fakeNow);
expect(dbSearchEngine!.integrationId).toBe(dbIntegration!.id);
expect(dbSearchEngine!.short).toBe("j");
expect(dbSearchEngine!.name).toBe(input.name);
expect(dbSearchEngine!.iconUrl).toBe(
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/jellyseerr.png",
);
});
test("without create integration access should throw permission error", async () => {
// Arrange
const db = createDb();
@@ -213,6 +256,7 @@ describe("create should create a new integration", () => {
kind: "jellyfin" as const,
url: "http://jellyfin.local",
secrets: [{ kind: "apiKey" as const, value: "1234567890" }],
attemptSearchEngineCreation: false,
};
// Act
+2
View File
@@ -8,6 +8,7 @@ import { indexerManagerRouter } from "./indexer-manager";
import { mediaRequestsRouter } from "./media-requests";
import { mediaServerRouter } from "./media-server";
import { mediaTranscodingRouter } from "./media-transcoding";
import { minecraftRouter } from "./minecraft";
import { notebookRouter } from "./notebook";
import { rssFeedRouter } from "./rssFeed";
import { smartHomeRouter } from "./smart-home";
@@ -27,4 +28,5 @@ export const widgetRouter = createTRPCRouter({
indexerManager: indexerManagerRouter,
healthMonitoring: healthMonitoringRouter,
mediaTranscoding: mediaTranscodingRouter,
minecraft: minecraftRouter,
});
@@ -21,6 +21,7 @@ export const mediaServerRouter = createTRPCRouter({
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return {
integrationId: integration.id,
integrationKind: integration.kind,
sessions: data,
};
}),
@@ -0,0 +1,36 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import type { MinecraftServerStatus } from "@homarr/request-handler/minecraft-server-status";
import { minecraftServerStatusRequestHandler } from "@homarr/request-handler/minecraft-server-status";
import { createTRPCRouter, publicProcedure } from "../../trpc";
const serverStatusInputSchema = z.object({
domain: z.string().nonempty(),
isBedrockServer: z.boolean(),
});
export const minecraftRouter = createTRPCRouter({
getServerStatus: publicProcedure.input(serverStatusInputSchema).query(async ({ input }) => {
const innerHandler = minecraftServerStatusRequestHandler.handler({
isBedrockServer: input.isBedrockServer,
domain: input.domain,
});
return await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true });
}),
subscribeServerStatus: publicProcedure.input(serverStatusInputSchema).subscription(({ input }) => {
return observable<MinecraftServerStatus>((emit) => {
const innerHandler = minecraftServerStatusRequestHandler.handler({
isBedrockServer: input.isBedrockServer,
domain: input.domain,
});
const unsubscribe = innerHandler.subscribe((data) => {
emit.next(data);
});
return () => {
unsubscribe();
};
});
}),
});
-2
View File
@@ -64,7 +64,6 @@ export const env = createEnv({
server: {
AUTH_LOGOUT_REDIRECT_URL: z.string().url().optional(),
AUTH_SESSION_EXPIRY_TIME: createDurationSchema("30d"),
AUTH_SECRET: process.env.NODE_ENV === "production" ? z.string().min(1) : z.string().min(1).optional(),
AUTH_PROVIDERS: authProvidersSchema,
...(authProviders.includes("oidc")
? {
@@ -98,7 +97,6 @@ export const env = createEnv({
runtimeEnv: {
AUTH_LOGOUT_REDIRECT_URL: process.env.AUTH_LOGOUT_REDIRECT_URL,
AUTH_SESSION_EXPIRY_TIME: process.env.AUTH_SESSION_EXPIRY_TIME,
AUTH_SECRET: process.env.AUTH_SECRET,
AUTH_PROVIDERS: process.env.AUTH_PROVIDERS,
AUTH_LDAP_BASE: process.env.AUTH_LDAP_BASE,
AUTH_LDAP_BIND_DN: process.env.AUTH_LDAP_BIND_DN,
+1 -1
View File
@@ -34,7 +34,7 @@
"bcrypt": "^5.1.1",
"cookies": "^0.9.1",
"ldapts": "7.3.0",
"next": "^14.2.21",
"next": "^14.2.22",
"next-auth": "5.0.0-beta.25",
"react": "^19.0.0",
"react-dom": "^19.0.0"
+28
View File
@@ -0,0 +1,28 @@
import { randomBytes } from "crypto";
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
const errorSuffix = `, please generate a 64 character secret in hex format or use the following: "${randomBytes(32).toString("hex")}"`;
export const env = createEnv({
server: {
SECRET_ENCRYPTION_KEY: z
.string({
required_error: `SECRET_ENCRYPTION_KEY is required${errorSuffix}`,
})
.min(64, {
message: `SECRET_ENCRYPTION_KEY has to be 64 characters${errorSuffix}`,
})
.max(64, {
message: `SECRET_ENCRYPTION_KEY has to be 64 characters${errorSuffix}`,
})
.regex(/^[0-9a-fA-F]{64}$/, {
message: `SECRET_ENCRYPTION_KEY must only contain hex characters${errorSuffix}`,
}),
},
runtimeEnv: {
SECRET_ENCRYPTION_KEY: process.env.SECRET_ENCRYPTION_KEY,
},
skipValidation:
Boolean(process.env.CI) || Boolean(process.env.SKIP_ENV_VALIDATION) || process.env.npm_lifecycle_event === "lint",
});
+3 -2
View File
@@ -8,7 +8,8 @@
".": "./index.ts",
"./types": "./src/types.ts",
"./server": "./src/server.ts",
"./client": "./src/client.ts"
"./client": "./src/client.ts",
"./env.mjs": "./env.mjs"
},
"typesVersions": {
"*": {
@@ -27,7 +28,7 @@
"dependencies": {
"@homarr/log": "workspace:^0.1.0",
"dayjs": "^1.11.13",
"next": "^14.2.21",
"next": "^14.2.22",
"react": "^19.0.0"
},
"devDependencies": {
+4 -12
View File
@@ -1,20 +1,12 @@
import crypto from "crypto";
import { logger } from "@homarr/log";
import { env } from "../env.mjs";
const algorithm = "aes-256-cbc"; //Using AES encryption
const fallbackKey = "0000000000000000000000000000000000000000000000000000000000000000";
const encryptionKey = process.env.ENCRYPTION_KEY ?? fallbackKey; // Fallback to a default key for local development
if (encryptionKey === fallbackKey) {
logger.warn("Using a fallback encryption key, stored secrets are not secure");
// We never want to use the fallback key in production
if (process.env.NODE_ENV === "production" && process.env.CI !== "true") {
throw new Error("Encryption key is not set");
}
}
const key = Buffer.from(encryptionKey, "hex");
// We fallback to a key of 0s if the key was not provided because env validation was skipped
// This should only be the case in CI
const key = Buffer.from(env.SECRET_ENCRYPTION_KEY || "0".repeat(64), "hex");
export function encryptSecret(text: string): `${string}.${string}` {
const initializationVector = crypto.randomBytes(16);
+5
View File
@@ -11,3 +11,8 @@ export type RemoveReadonly<T> = {
};
export type MaybeArray<T> = T | T[];
export type Inverse<T extends Invertible> = {
[Key in keyof T as T[Key]]: Key;
};
type Invertible = Record<PropertyKey, PropertyKey>;
+2
View File
@@ -9,6 +9,7 @@ import { mediaOrganizerJob } from "./jobs/integrations/media-organizer";
import { mediaRequestListJob, mediaRequestStatsJob } from "./jobs/integrations/media-requests";
import { mediaServerJob } from "./jobs/integrations/media-server";
import { mediaTranscodingJob } from "./jobs/integrations/media-transcoding";
import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
import { pingJob } from "./jobs/ping";
import type { RssFeed } from "./jobs/rss-feeds";
import { rssFeedsJob } from "./jobs/rss-feeds";
@@ -33,6 +34,7 @@ export const jobGroup = createCronJobGroup({
sessionCleanup: sessionCleanupJob,
updateChecker: updateCheckerJob,
mediaTranscoding: mediaTranscodingJob,
minecraftServerStatus: minecraftServerStatusJob,
});
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
@@ -0,0 +1,25 @@
import SuperJSON from "superjson";
import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions";
import { db, eq } from "@homarr/db";
import { items } from "@homarr/db/schema";
import { minecraftServerStatusRequestHandler } from "@homarr/request-handler/minecraft-server-status";
import type { WidgetComponentProps } from "../../../widgets/src";
import { createCronJob } from "../lib";
export const minecraftServerStatusJob = createCronJob("minecraftServerStatus", EVERY_5_MINUTES).withCallback(
async () => {
const dbItems = await db.query.items.findMany({
where: eq(items.kind, "minecraftServerStatus"),
});
await Promise.allSettled(
dbItems.map(async (item) => {
const options = SuperJSON.parse<WidgetComponentProps<"minecraftServerStatus">["options"]>(item.options);
const innerHandler = minecraftServerStatusRequestHandler.handler(options);
await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true });
}),
);
},
);
@@ -0,0 +1 @@
ALTER TABLE `search_engine` ADD CONSTRAINT `search_engine_short_unique` UNIQUE(`short`);
File diff suppressed because it is too large. Load diff
@@ -127,6 +127,13 @@
"when": 1733777544067,
"tag": "0017_tired_penance",
"breakpoints": true
},
{
"idx": 18,
"version": "5",
"when": 1735593853768,
"tag": "0018_mighty_shaman",
"breakpoints": true
}
]
}
@@ -0,0 +1 @@
CREATE UNIQUE INDEX `search_engine_short_unique` ON `search_engine` (`short`);
File diff suppressed because it is too large. Load diff
@@ -127,6 +127,13 @@
"when": 1733777395703,
"tag": "0017_small_rumiko_fujikawa",
"breakpoints": true
},
{
"idx": 18,
"version": "6",
"when": 1735593831501,
"tag": "0018_cheerful_tattoo",
"breakpoints": true
}
]
}
+2 -2
View File
@@ -48,8 +48,8 @@
"better-sqlite3": "^11.7.0",
"dotenv": "^16.4.7",
"drizzle-kit": "^0.30.1",
"drizzle-orm": "^0.38.2",
"drizzle-zod": "^0.6.0",
"drizzle-orm": "^0.38.3",
"drizzle-zod": "^0.6.1",
"mysql2": "3.12.0"
},
"devDependencies": {
+1 -1
View File
@@ -389,7 +389,7 @@ export const searchEngines = mysqlTable("search_engine", {
id: varchar({ length: 64 }).notNull().primaryKey(),
iconUrl: text().notNull(),
name: varchar({ length: 64 }).notNull(),
short: varchar({ length: 8 }).notNull(),
short: varchar({ length: 8 }).unique().notNull(),
description: text(),
urlTemplate: text(),
type: varchar({ length: 64 }).$type<SearchEngineType>().notNull().default("generic"),
+1 -1
View File
@@ -375,7 +375,7 @@ export const searchEngines = sqliteTable("search_engine", {
id: text().notNull().primaryKey(),
iconUrl: text().notNull(),
name: text().notNull(),
short: text().notNull(),
short: text().unique().notNull(),
description: text(),
urlTemplate: text(),
type: text().$type<SearchEngineType>().notNull().default("generic"),
@@ -90,6 +90,7 @@ export type HomarrDocumentationPath =
| "/docs/tags/lists"
| "/docs/tags/management"
| "/docs/tags/media"
| "/docs/tags/minecraft"
| "/docs/tags/monitoring"
| "/docs/tags/news"
| "/docs/tags/notebook"
@@ -190,6 +191,7 @@ export type HomarrDocumentationPath =
| "/docs/widgets/indexer-manager"
| "/docs/widgets/media-requests"
| "/docs/widgets/media-server"
| "/docs/widgets/minecraft-server-status"
| "/docs/widgets/notebook"
| "/docs/widgets/rss"
| "/docs/widgets/video"
+1
View File
@@ -15,6 +15,7 @@ export const widgetKinds = [
"mediaRequests-requestList",
"mediaRequests-requestStats",
"mediaTranscoding",
"minecraftServerStatus",
"rssFeed",
"bookmarks",
"indexerManager",
+1
View File
@@ -1,2 +1,3 @@
export * from "./src/icons-fetcher";
export * from "./src/types";
export * from "./src/auto-icon-searcher";
+9
View File
@@ -0,0 +1,9 @@
import type { Database } from "@homarr/db";
import { like } from "@homarr/db";
import { icons } from "@homarr/db/schema";
export const getIconForNameAsync = async (db: Database, name: string) => {
return await db.query.icons.findFirst({
where: like(icons.name, `%${name}%`),
});
};
+2 -2
View File
@@ -24,9 +24,9 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@ctrl/deluge": "^7.0.0",
"@ctrl/deluge": "^7.1.0",
"@ctrl/qbittorrent": "^9.2.0",
"@ctrl/transmission": "^7.1.0",
"@ctrl/transmission": "^7.2.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
@@ -1,3 +1,3 @@
export interface ISearchableIntegration {
searchAsync(query: string): Promise<{ image?: string; name: string; link: string }[]>;
export interface ISearchableIntegration<TResult extends { image?: string; name: string; link: string }> {
searchAsync(query: string): Promise<TResult[]>;
}
@@ -6,11 +6,20 @@ import type { ISearchableIntegration } from "../base/searchable-integration";
import type { MediaRequest, RequestStats, RequestUser } from "../interfaces/media-requests/media-request";
import { MediaAvailability, MediaRequestStatus } from "../interfaces/media-requests/media-request";
interface OverseerrSearchResult {
id: number;
name: string;
link: string;
image?: string;
text?: string;
type: Exclude<z.infer<typeof searchSchema>["results"], undefined>[number]["mediaType"];
}
/**
* Overseerr Integration. See https://api-docs.overseerr.dev
*/
export class OverseerrIntegration extends Integration implements ISearchableIntegration {
public async searchAsync(query: string): Promise<{ image?: string; name: string; link: string; text?: string }[]> {
export class OverseerrIntegration extends Integration implements ISearchableIntegration<OverseerrSearchResult> {
public async searchAsync(query: string) {
const response = await fetch(this.url("/api/v1/search", { query }), {
headers: {
"X-Api-Key": this.getSecretValue("apiKey"),
@@ -23,13 +32,53 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
}
return schemaData.results.map((result) => ({
id: result.id,
name: "name" in result ? result.name : result.title,
link: this.url(`/${result.mediaType}/${result.id}`).toString(),
image: constructSearchResultImage(result),
text: "overview" in result ? result.overview : undefined,
type: result.mediaType,
inLibrary: result.mediaInfo !== undefined,
}));
}
public async getSeriesInformationAsync(mediaType: "movie" | "tv", id: number) {
const url = mediaType === "tv" ? this.url(`/api/v1/tv/${id}`) : this.url(`/api/v1/movie/${id}`);
const response = await fetch(url, {
headers: {
"X-Api-Key": this.getSecretValue("apiKey"),
},
});
return await mediaInformationSchema.parseAsync(await response.json());
}
/**
* Request a media. See https://api-docs.overseerr.dev/#/request/post_request
* @param mediaType The media type to request. Can be "movie" or "tv".
* @param id The Overseerr ID of the media to request.
* @param seasons A list of the seasons that should be requested.
*/
public async requestMediaAsync(mediaType: "movie" | "tv", id: number, seasons?: number[]): Promise<void> {
const url = this.url("/api/v1/request");
const response = await fetch(url, {
method: "POST",
body: JSON.stringify({
mediaType,
mediaId: id,
seasons,
}),
headers: {
"X-Api-Key": this.getSecretValue("apiKey"),
"Content-Type": "application/json",
},
});
if (response.status !== 201) {
throw new Error(
`Status code ${response.status} does not match the expected status code. The request was likely not created. Response: ${await response.text()}`,
);
}
}
public async testConnectionAsync(): Promise<void> {
const response = await fetch(this.url("/api/v1/auth/me"), {
headers: {
@@ -220,6 +269,27 @@ interface MovieInformation {
releaseDate: string;
}
const mediaInformationSchema = z.union([
z.object({
id: z.number(),
overview: z.string(),
seasons: z.array(
z.object({
id: z.number(),
name: z.string().min(0),
episodeCount: z.number().min(0),
}),
),
numberOfSeasons: z.number(),
posterPath: z.string().startsWith("/"),
}),
z.object({
id: z.number(),
overview: z.string(),
posterPath: z.string().startsWith("/"),
}),
]);
const searchSchema = z.object({
results: z
.array(
@@ -230,6 +300,7 @@ const searchSchema = z.object({
name: z.string(),
posterPath: z.string().startsWith("/").endsWith(".jpg").nullable(),
overview: z.string(),
mediaInfo: z.object({}).optional(),
}),
z.object({
id: z.number(),
@@ -237,12 +308,14 @@ const searchSchema = z.object({
title: z.string(),
posterPath: z.string().startsWith("/").endsWith(".jpg").nullable(),
overview: z.string(),
mediaInfo: z.object({}).optional(),
}),
z.object({
id: z.number(),
mediaType: z.literal("person"),
name: z.string(),
profilePath: z.string().startsWith("/").endsWith(".jpg").nullable(),
mediaInfo: z.object({}).optional(),
}),
]),
)
+1 -1
View File
@@ -35,7 +35,7 @@
"@mantine/core": "^7.15.2",
"@tabler/icons-react": "^3.26.0",
"dayjs": "^1.11.13",
"next": "^14.2.21",
"next": "^14.2.22",
"react": "^19.0.0"
},
"devDependencies": {
+1
View File
@@ -1,3 +1,4 @@
export * from "./boards";
export * from "./invites";
export * from "./groups";
export * from "./search-engines";
@@ -0,0 +1 @@
export { RequestMediaModal } from "./request-media-modal";
@@ -0,0 +1,119 @@
import { useMemo } from "react";
import { Button, Group, Image, LoadingOverlay, Stack, Text } from "@mantine/core";
import type { MRT_ColumnDef } from "mantine-react-table";
import { MRT_Table } from "mantine-react-table";
import { clientApi } from "@homarr/api/client";
import { createModal } from "@homarr/modals";
import { showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
interface RequestMediaModalProps {
integrationId: string;
mediaId: number;
mediaType: "movie" | "tv";
}
export const RequestMediaModal = createModal<RequestMediaModalProps>(({ actions, innerProps }) => {
const { data, isPending: isPendingQuery } = clientApi.searchEngine.getMediaRequestOptions.useQuery({
integrationId: innerProps.integrationId,
mediaId: innerProps.mediaId,
mediaType: innerProps.mediaType,
});
const { mutate, isPending: isPendingMutation } = clientApi.searchEngine.requestMedia.useMutation({
onSuccess() {
actions.closeModal();
showSuccessNotification({
message: t("common.notification.create.success"),
});
},
});
const isPending = isPendingQuery || isPendingMutation;
const t = useI18n();
const columns = useMemo<MRT_ColumnDef<Season>[]>(
() => [
{
accessorKey: "name",
header: t("search.engine.media.request.modal.table.header.season"),
},
{
accessorKey: "episodeCount",
header: t("search.engine.media.request.modal.table.header.episodes"),
},
],
[],
);
const table = useTranslatedMantineReactTable({
columns,
data: data && "seasons" in data ? data.seasons : [],
enableColumnActions: false,
enableColumnFilters: false,
enablePagination: false,
enableSorting: false,
enableSelectAll: true,
enableRowSelection: true,
mantineTableProps: {
highlightOnHover: false,
striped: "odd",
withColumnBorders: true,
withRowBorders: true,
withTableBorder: true,
},
initialState: {
density: "xs",
},
});
const anySelected = Object.keys(table.getState().rowSelection).length > 0;
const handleMutate = () => {
const selectedSeasons = table.getSelectedRowModel().rows.flatMap((row) => row.original.id);
mutate({
integrationId: innerProps.integrationId,
mediaId: innerProps.mediaId,
mediaType: innerProps.mediaType,
seasons: selectedSeasons,
});
};
return (
<Stack>
<LoadingOverlay visible={isPending} zIndex={1000} overlayProps={{ radius: "sm", blur: 2 }} />
{data && (
<Group wrap="nowrap" align="start">
<Image
src={`https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`}
alt="poster"
w={100}
radius="md"
/>
<Text c="dimmed" style={{ flex: "1" }}>
{data.overview}
</Text>
</Group>
)}
{innerProps.mediaType === "tv" && <MRT_Table table={table} />}
<Group justify="end">
<Button onClick={actions.closeModal} variant="light">
{t("common.action.cancel")}
</Button>
<Button onClick={handleMutate} disabled={!anySelected && innerProps.mediaType === "tv"}>
{t("search.engine.media.request.modal.button.send")}
</Button>
</Group>
</Stack>
);
}).withOptions({
size: "xl",
});
interface Season {
id: number;
name: string;
episodeCount: number;
}
+1 -1
View File
@@ -40,7 +40,7 @@
"@mantine/core": "^7.15.2",
"@mantine/hooks": "^7.15.2",
"adm-zip": "0.5.16",
"next": "^14.2.21",
"next": "^14.2.22",
"react": "^19.0.0",
"superjson": "2.2.2",
"zod": "^3.24.1",
+1 -1
View File
@@ -55,7 +55,7 @@ export const insertItemsAsync = async (
xOffset: screenSizeShape.location.x,
yOffset: screenSizeShape.location.y,
kind,
options: SuperJSON.stringify(mapOptions(kind, widget.properties, appsMap)),
options: SuperJSON.stringify(mapOptions(widget.type, widget.properties, appsMap)),
});
logger.debug(`Inserted widget id=${widget.id} sectionId=${sectionId}`);
+5 -1
View File
@@ -83,7 +83,11 @@ export const mapWidget = (
yOffset: shapeForSize.location.y,
kind,
options: SuperJSON.stringify(
mapOptions(kind, widget.properties, new Map([...appsMap.entries()].map(([key, value]) => [key, value.id]))),
mapOptions(
widget.type,
widget.properties,
new Map([...appsMap.entries()].map(([key, value]) => [key, value.id])),
),
),
};
};
@@ -23,6 +23,7 @@ export const mapAndDecryptUsers = (importUsers: OldmarrImportUser[], encryptionT
...user,
oldId: id,
id: createId(),
name: user.name.toLowerCase(),
colorScheme: settings?.colorScheme === "environment" ? undefined : settings?.colorScheme,
firstDayOfWeek: settings?.firstDayOfWeek === "sunday" ? 0 : settings?.firstDayOfWeek === "monday" ? 1 : 6,
provider: "credentials",
@@ -1,4 +1,5 @@
import { objectEntries } from "@homarr/common";
import type { Inverse } from "@homarr/common/types";
import type { WidgetKind } from "@homarr/definitions";
import type { OldmarrBookmarkDefinition } from "./bookmark";
@@ -49,32 +50,32 @@ export type OldmarrWidgetDefinitions =
| OldmarrMediaTranscodingDefinition;
export const widgetKindMapping = {
app: null, // In oldmarr apps were not widgets
clock: "date",
date: "clock",
calendar: "calendar",
downloads: "torrents-status",
"torrents-status": "downloads",
weather: "weather",
rssFeed: "rss",
video: "video-stream",
rss: "rssFeed",
"video-stream": "video",
iframe: "iframe",
mediaServer: "media-server",
dnsHoleSummary: "dns-hole-summary",
dnsHoleControls: "dns-hole-controls",
"media-server": "mediaServer",
"dns-hole-summary": "dnsHoleSummary",
"dns-hole-controls": "dnsHoleControls",
notebook: "notebook",
"smartHome-entityState": "smart-home/entity-state",
"smartHome-executeAutomation": "smart-home/trigger-automation",
"mediaRequests-requestList": "media-requests-list",
"mediaRequests-requestStats": "media-requests-stats",
indexerManager: "indexer-manager",
bookmarks: "bookmark",
healthMonitoring: "health-monitoring",
mediaTranscoding: "media-transcoding",
} satisfies Record<WidgetKind, OldmarrWidgetDefinitions["id"] | null>;
// Use null for widgets that did not exist in oldmarr
// TODO: revert assignment so that only old widgets are needed in the object,
// this can be done ones all widgets are implemented
"smart-home/entity-state": "smartHome-entityState",
"smart-home/trigger-automation": "smartHome-executeAutomation",
"media-requests-list": "mediaRequests-requestList",
"media-requests-stats": "mediaRequests-requestStats",
"indexer-manager": "indexerManager",
bookmark: "bookmarks",
"health-monitoring": "healthMonitoring",
dashdot: "healthMonitoring",
"media-transcoding": "mediaTranscoding",
dlspeed: null,
usenet: "downloads",
} satisfies Record<OldmarrWidgetDefinitions["id"], WidgetKind | null>;
export type WidgetMapping = typeof widgetKindMapping;
export type InversedWidgetMapping = Inverse<Omit<typeof widgetKindMapping, "dlspeed">>;
export const mapKind = (kind: OldmarrWidgetDefinitions["id"]): WidgetKind | undefined =>
objectEntries(widgetKindMapping).find(([_, value]) => value === kind)?.[0];
export const mapKind = (kind: OldmarrWidgetDefinitions["id"]): keyof InversedWidgetMapping | null =>
objectEntries(widgetKindMapping).find(([key]) => key === kind)?.[1] ?? null;
+39 -26
View File
@@ -1,18 +1,18 @@
import { objectEntries } from "@homarr/common";
import type { WidgetKind } from "@homarr/definitions";
import { logger } from "@homarr/log";
import type { WidgetComponentProps } from "../../../widgets/src/definition";
import type { OldmarrWidgetDefinitions, WidgetMapping } from "./definitions";
import { mapKind } from "./definitions";
import type { InversedWidgetMapping, OldmarrWidgetDefinitions, WidgetMapping } from "./definitions";
// This type enforces, that for all widget mappings there is a corresponding option mapping,
// each option of newmarr can be mapped from the value of the oldmarr options
type OptionMapping = {
[WidgetKey in keyof WidgetMapping]: WidgetMapping[WidgetKey] extends null
[WidgetKey in keyof InversedWidgetMapping]: InversedWidgetMapping[WidgetKey] extends null
? null
: {
[OptionsKey in keyof WidgetComponentProps<WidgetKey>["options"]]: (
oldOptions: Extract<OldmarrWidgetDefinitions, { id: WidgetMapping[WidgetKey] }>["options"],
oldOptions: Extract<OldmarrWidgetDefinitions, { id: InversedWidgetMapping[WidgetKey] }>["options"],
appsMap: Map<string, string>,
) => WidgetComponentProps<WidgetKey>["options"][OptionsKey] | undefined;
};
@@ -55,12 +55,16 @@ const optionMapping: OptionMapping = {
useCustomTimezone: () => true,
},
downloads: {
activeTorrentThreshold: (oldOptions) => oldOptions.speedLimitOfActiveTorrents,
applyFilterToRatio: (oldOptions) => oldOptions.displayRatioWithFilter,
categoryFilter: (oldOptions) => oldOptions.labelFilter,
filterIsWhitelist: (oldOptions) => oldOptions.labelFilterIsWhitelist,
enableRowSorting: (oldOptions) => oldOptions.rowSorting,
showCompletedTorrent: (oldOptions) => oldOptions.displayCompletedTorrents,
activeTorrentThreshold: (oldOptions) =>
"speedLimitOfActiveTorrents" in oldOptions ? oldOptions.speedLimitOfActiveTorrents : undefined,
applyFilterToRatio: (oldOptions) =>
"displayRatioWithFilter" in oldOptions ? oldOptions.displayRatioWithFilter : undefined,
categoryFilter: (oldOptions) => ("labelFilter" in oldOptions ? oldOptions.labelFilter : undefined),
filterIsWhitelist: (oldOptions) =>
"labelFilterIsWhitelist" in oldOptions ? oldOptions.labelFilterIsWhitelist : undefined,
enableRowSorting: (oldOptions) => ("rowSorting" in oldOptions ? oldOptions.rowSorting : undefined),
showCompletedTorrent: (oldOptions) =>
"displayCompletedTorrents" in oldOptions ? oldOptions.displayCompletedTorrents : undefined,
columns: () => ["integration", "name", "progress", "time", "actions"],
defaultSort: () => "type",
descendingDefaultSort: () => false,
@@ -124,45 +128,54 @@ const optionMapping: OptionMapping = {
openIndexerSiteInNewTab: (oldOptions) => oldOptions.openIndexerSiteInNewTab,
},
healthMonitoring: {
cpu: (oldOptions) => oldOptions.cpu,
memory: (oldOptions) => oldOptions.memory,
fahrenheit: (oldOptions) => oldOptions.fahrenheit,
fileSystem: (oldOptions) => oldOptions.fileSystem,
cpu: (oldOptions) =>
"cpu" in oldOptions
? oldOptions.cpu
: oldOptions.graphsOrder.some((graph) => graph.key === "cpu" && graph.subValues.enabled),
memory: (oldOptions) =>
"memory" in oldOptions
? oldOptions.memory
: oldOptions.graphsOrder.some((graph) => graph.key === "ram" && graph.subValues.enabled),
fahrenheit: (oldOptions) => ("fahrenheit" in oldOptions ? oldOptions.fahrenheit : undefined),
fileSystem: (oldOptions) =>
"fileSystem" in oldOptions
? oldOptions.fileSystem
: oldOptions.graphsOrder.some((graph) => graph.key === "storage" && graph.subValues.enabled),
},
mediaTranscoding: {
defaultView: (oldOptions) => oldOptions.defaultView,
queuePageSize: (oldOptions) => oldOptions.queuePageSize,
},
app: null,
};
/**
* Maps the oldmarr options to the newmarr options
* @param kind item kind to map
* @param type old widget type
* @param oldOptions oldmarr options for this item
* @param appsMap map of old app ids to new app ids
* @returns newmarr options for this item or null if the item did not exist in oldmarr
*/
export const mapOptions = <K extends WidgetKind>(
kind: K,
oldOptions: Extract<OldmarrWidgetDefinitions, { id: WidgetMapping[K] }>["options"],
export const mapOptions = <K extends OldmarrWidgetDefinitions["id"]>(
type: K,
oldOptions: Extract<OldmarrWidgetDefinitions, { id: K }>["options"],
appsMap: Map<string, string>,
) => {
logger.debug(`Mapping old homarr options for widget kind=${kind} options=${JSON.stringify(oldOptions)}`);
if (optionMapping[kind] === null) {
logger.debug(`Mapping old homarr options for widget type=${type} options=${JSON.stringify(oldOptions)}`);
const kind = mapKind(type);
if (!kind) {
return null;
}
const mapping = optionMapping[kind];
return objectEntries(mapping).reduce(
(acc, [key, value]) => {
const newValue = value(oldOptions as never, appsMap);
logger.debug(`Mapping old homarr option kind=${kind} key=${key as string} newValue=${newValue as string}`);
(acc, [key, value]: [string, (oldOptions: Record<string, unknown>, appsMap: Map<string, string>) => unknown]) => {
const newValue = value(oldOptions, appsMap);
logger.debug(`Mapping old homarr option kind=${kind} key=${key} newValue=${newValue as string}`);
if (newValue !== undefined) {
acc[key as string] = newValue;
acc[key] = newValue;
}
return acc;
},
{} as Record<string, unknown>,
) as WidgetComponentProps<K>["options"];
) as WidgetComponentProps<Exclude<WidgetMapping[K], null>>["options"];
};
+1
View File
@@ -5,6 +5,7 @@ export {
createItemAndIntegrationChannel,
createItemChannel,
createIntegrationOptionsChannel,
createWidgetOptionsChannel,
createChannelWithLatestAndEvents,
handshakeAsync,
createSubPubChannel,
+10
View File
@@ -183,6 +183,16 @@ export const createIntegrationOptionsChannel = <TData>(
return createChannelWithLatestAndEvents<TData>(channelName);
};
export const createWidgetOptionsChannel = <TData>(
widgetKind: WidgetKind,
queryKey: string,
options: Record<string, unknown>,
) => {
const optionsKey = hashObjectBase64(options);
const channelName = `widget:${widgetKind}:${queryKey}:options:${optionsKey}`;
return createChannelWithLatestAndEvents<TData>(channelName);
};
export const createItemChannel = <TData>(itemId: string) => {
return createChannelWithLatestAndEvents<TData>(`item:${itemId}`);
};
+1 -1
View File
@@ -29,7 +29,7 @@
"@homarr/log": "workspace:^0.1.0",
"@homarr/redis": "workspace:^0.1.0",
"dayjs": "^1.11.13",
"octokit": "^4.0.2",
"octokit": "^4.0.3",
"pretty-print-error": "^1.1.2",
"superjson": "2.2.2"
},
@@ -0,0 +1,36 @@
import type { Duration } from "dayjs/plugin/duration";
import type { WidgetKind } from "@homarr/definitions";
import { createWidgetOptionsChannel } from "@homarr/redis";
import { createCachedRequestHandler } from "./cached-request-handler";
interface Options<TData, TKind extends WidgetKind, TInput extends Record<string, unknown>> {
// Unique key for this request handler
queryKey: string;
requestAsync: (input: TInput) => Promise<TData>;
cacheDuration: Duration;
widgetKind: TKind;
}
export const createCachedWidgetRequestHandler = <
TData,
TKind extends WidgetKind,
TInput extends Record<string, unknown>,
>(
requestHandlerOptions: Options<TData, TKind, TInput>,
) => {
return {
handler: (widgetOptions: TInput) =>
createCachedRequestHandler({
queryKey: requestHandlerOptions.queryKey,
requestAsync: async (input: TInput) => {
return await requestHandlerOptions.requestAsync(input);
},
cacheDuration: requestHandlerOptions.cacheDuration,
createRedisChannel(input, options) {
return createWidgetOptionsChannel<TData>(requestHandlerOptions.widgetKind, options.queryKey, input);
},
}).handler(widgetOptions),
};
};
@@ -0,0 +1,35 @@
import dayjs from "dayjs";
import { z } from "zod";
import { fetchWithTimeout } from "@homarr/common";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
export const minecraftServerStatusRequestHandler = createCachedWidgetRequestHandler({
queryKey: "minecraftServerStatusApiResult",
widgetKind: "minecraftServerStatus",
async requestAsync(input: { domain: string; isBedrockServer: boolean }) {
const path = `/3/${input.isBedrockServer ? "bedrock/" : ""}${input.domain}`;
const response = await fetchWithTimeout(`https://api.mcsrvstat.us${path}`);
return responseSchema.parse(await response.json());
},
cacheDuration: dayjs.duration(5, "minutes"),
});
const responseSchema = z
.object({
online: z.literal(false),
})
.or(
z.object({
online: z.literal(true),
players: z.object({
online: z.number(),
max: z.number(),
}),
icon: z.string().optional(),
}),
);
export type MinecraftServerStatus = z.infer<typeof responseSchema>;
+1 -1
View File
@@ -37,7 +37,7 @@
"@mantine/spotlight": "^7.15.2",
"@tabler/icons-react": "^3.26.0",
"jotai": "^2.11.0",
"next": "^14.2.21",
"next": "^14.2.22",
"react": "^19.0.0",
"use-deep-compare-effect": "^1.8.1"
},
@@ -4,14 +4,25 @@ import { ChildrenActionItem } from "./items/children-action-item";
interface SpotlightChildrenActionsProps {
childrenOptions: inferSearchInteractionOptions<"children">;
query: string;
setChildrenOptions: (options: inferSearchInteractionOptions<"children">) => void;
}
export const SpotlightChildrenActions = ({ childrenOptions, query }: SpotlightChildrenActionsProps) => {
export const SpotlightChildrenActions = ({
childrenOptions,
query,
setChildrenOptions,
}: SpotlightChildrenActionsProps) => {
const actions = childrenOptions.useActions(childrenOptions.option, query);
return actions
.filter((action) => (typeof action.hide === "function" ? !action.hide(childrenOptions.option) : !action.hide))
.map((action) => (
<ChildrenActionItem key={action.key} childrenOptions={childrenOptions} query={query} action={action} />
<ChildrenActionItem
key={action.key}
childrenOptions={childrenOptions}
query={query}
action={action}
setChildrenOptions={setChildrenOptions}
/>
));
};
@@ -8,9 +8,10 @@ interface ChildrenActionItemProps {
childrenOptions: inferSearchInteractionOptions<"children">;
query: string;
action: ReturnType<inferSearchInteractionOptions<"children">["useActions"]>[number];
setChildrenOptions: (options: inferSearchInteractionOptions<"children">) => void;
}
export const ChildrenActionItem = ({ childrenOptions, action, query }: ChildrenActionItemProps) => {
export const ChildrenActionItem = ({ childrenOptions, action, query, setChildrenOptions }: ChildrenActionItemProps) => {
const interaction = action.useInteraction(childrenOptions.option, query);
const renderRoot =
@@ -20,10 +21,20 @@ export const ChildrenActionItem = ({ childrenOptions, action, query }: ChildrenA
}
: undefined;
const onClick = interaction.type === "javaScript" ? interaction.onSelect : undefined;
const onClick =
interaction.type === "javaScript"
? interaction.onSelect
: interaction.type === "children"
? () => setChildrenOptions(interaction)
: undefined;
return (
<Spotlight.Action renderRoot={renderRoot} onClick={onClick} className={classes.spotlightAction}>
<Spotlight.Action
renderRoot={renderRoot}
onClick={onClick}
closeSpotlightOnTrigger={interaction.type !== "children"}
className={classes.spotlightAction}
>
<action.Component {...childrenOptions.option} />
</Spotlight.Action>
);
@@ -140,7 +140,11 @@ const SpotlightWithActiveMode = ({ modeState, activeMode, defaultMode }: Spotlig
<MantineSpotlight.ActionsList>
{childrenOptions ? (
<SpotlightChildrenActions childrenOptions={childrenOptions} query={query} />
<SpotlightChildrenActions
childrenOptions={childrenOptions}
query={query}
setChildrenOptions={setChildrenOptions}
/>
) : (
<SpotlightActionGroups
setMode={(mode) => {
+4 -1
View File
@@ -10,7 +10,10 @@ export interface CreateChildrenOptionsProps<TParentOptions extends Record<string
export interface ChildrenAction<TParentOptions extends Record<string, unknown>> {
key: string;
Component: (option: TParentOptions) => JSX.Element;
useInteraction: (option: TParentOptions, query: string) => inferSearchInteractionDefinition<"link" | "javaScript">;
useInteraction: (
option: TParentOptions,
query: string,
) => inferSearchInteractionDefinition<"link" | "javaScript" | "children">;
hide?: boolean | ((option: TParentOptions) => boolean);
}
@@ -1,8 +1,12 @@
import { Group, Image, Kbd, Stack, Text } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react";
import { IconDownload, IconSearch } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import type { IntegrationKind } from "@homarr/definitions";
import { getIntegrationKindsByCategory, getIntegrationName } from "@homarr/definitions";
import { useModalAction } from "@homarr/modals";
import { RequestMediaModal } from "@homarr/modals-collection";
import { useScopedI18n } from "@homarr/translation/client";
import { createChildrenOptions } from "../../lib/children";
@@ -11,6 +15,100 @@ import { interaction } from "../../lib/interaction";
type SearchEngine = RouterOutputs["searchEngine"]["search"][number];
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type MediaRequestChildrenProps = {
result: {
id: number;
image?: string;
name: string;
link: string;
text?: string;
type: "tv" | "movie";
inLibrary: boolean;
};
integration: {
kind: IntegrationKind;
url: string;
id: string;
};
};
const mediaRequestsChildrenOptions = createChildrenOptions<MediaRequestChildrenProps>({
useActions() {
const { openModal } = useModalAction(RequestMediaModal);
return [
{
key: "request",
hide: (option) => option.result.inLibrary,
Component(option) {
const t = useScopedI18n("search.mode.media");
return (
<Group mx="md" my="sm" wrap="nowrap">
<IconDownload stroke={1.5} />
{option.result.type === "tv" && <Text>{t("requestSeries")}</Text>}
{option.result.type === "movie" && <Text>{t("requestMovie")}</Text>}
</Group>
);
},
useInteraction: interaction.javaScript((option) => ({
onSelect() {
openModal(
{
integrationId: option.integration.id,
mediaId: option.result.id,
mediaType: option.result.type,
},
{
title(t) {
return t("search.engine.media.request.modal.title", { name: option.result.name });
},
},
);
},
})),
},
{
key: "open",
Component({ integration }) {
const tChildren = useScopedI18n("search.mode.media");
return (
<Group mx="md" my="sm" wrap="nowrap">
<IconSearch stroke={1.5} />
<Text>{tChildren("openIn", { kind: getIntegrationName(integration.kind) })}</Text>
</Group>
);
},
useInteraction({ result }) {
return {
type: "link",
href: result.link,
newTab: true,
};
},
},
];
},
DetailComponent({ options }) {
return (
<Group mx="md" my="sm" wrap="nowrap">
{options.result.image ? (
<Image src={options.result.image} w={35} h={50} fit="cover" radius={"md"} />
) : (
<IconSearch stroke={1.5} size={35} />
)}
<Stack gap={2}>
<Text>{options.result.name}</Text>
{options.result.text && (
<Text c="dimmed" size="sm" lineClamp={2}>
{options.result.text}
</Text>
)}
</Stack>
</Group>
);
},
});
export const searchEnginesChildrenOptions = createChildrenOptions<SearchEngine>({
useActions: (searchEngine, query) => {
const { data } = clientApi.integration.searchInIntegration.useQuery(
@@ -64,10 +162,48 @@ export const searchEnginesChildrenOptions = createChildrenOptions<SearchEngine>(
</Group>
);
},
useInteraction: interaction.link(() => ({
href: searchResult.link,
newTab: true,
})),
useInteraction(searchEngine) {
if (searchEngine.type !== "fromIntegration") {
throw new Error("Invalid search engine type");
}
if (!searchEngine.integration) {
throw new Error("Invalid search engine integration");
}
if (
getIntegrationKindsByCategory("mediaRequest").some(
(categoryKind) => categoryKind === searchEngine.integration?.kind,
) &&
"type" in searchResult
) {
const type = searchResult.type;
if (type === "person") {
return {
type: "link",
href: searchResult.link,
newTab: true,
};
}
return {
type: "children",
...mediaRequestsChildrenOptions({
result: {
...searchResult,
type,
},
integration: searchEngine.integration,
}),
};
}
return {
type: "link",
href: searchResult.link,
newTab: true,
};
},
}));
},
DetailComponent({ options }) {
+1 -1
View File
@@ -32,7 +32,7 @@
"dayjs": "^1.11.13",
"deepmerge": "4.3.1",
"mantine-react-table": "2.0.0-beta.7",
"next": "^14.2.21",
"next": "^14.2.22",
"next-intl": "3.26.3",
"react": "^19.0.0"
},
File diff suppressed because it is too large. Load diff
+7 -2
View File
@@ -1381,7 +1381,7 @@
"memory": "crwdns5734:0{memory}crwdne5734:0",
"memoryAvailable": "crwdns5736:0{memoryAvailable}crwdnd5736:0{percent}crwdne5736:0",
"version": "crwdns5738:0{version}crwdne5738:0",
"uptime": "crwdns5740:0{days}crwdnd5740:0{hours}crwdnd5740:0{minutes}crwdne5740:0",
"uptime": "crwdns7008:0{months}crwdnd7008:0{days}crwdnd7008:0{hours}crwdnd7008:0{minutes}crwdne7008:0",
"loadAverage": "crwdns5742:0crwdne5742:0",
"minute": "crwdns5744:0crwdne5744:0",
"minutes": "crwdns5746:0{count}crwdne5746:0",
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "crwdns5810:0crwdne5810:0",
"description": "crwdns5812:0crwdne5812:0",
"option": {}
"option": {},
"items": {
"user": "crwdns7010:0crwdne7010:0",
"name": "crwdns7012:0crwdne7012:0",
"id": "crwdns7014:0crwdne7014:0"
}
},
"downloads": {
"name": "crwdns5814:0crwdne5814:0",
+238 -233
View File
@@ -68,20 +68,20 @@
"subtitle": "",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Uživatel byl vytvořen",
"message": "Uživatel byl úspěšně vytvořen"
},
"error": {
"title": ""
"title": "Vytvoření uživatele se nezdařilo"
}
}
},
"group": {
"title": "",
"title": "Externí skupina",
"subtitle": "",
"form": {
"name": {
"label": "",
"label": "Název skupiny",
"description": ""
}
}
@@ -91,35 +91,35 @@
"subtitle": ""
},
"finish": {
"title": "",
"subtitle": "",
"description": "",
"title": "Dokončit nastavení",
"subtitle": "Jste připraveni začít!",
"description": "Úspěšně jste dokončili proces nastavení. Nyní můžete začít používat Homarr. Vyberte další akci:",
"action": {
"goToBoard": "",
"createBoard": "",
"inviteUser": "",
"docs": ""
"goToBoard": "Přejít na plochu {name}",
"createBoard": "Vytvořte svou první plochu",
"inviteUser": "Pozvat ostatní uživatele",
"docs": "Přečtěte si dokumentaci"
}
}
},
"backToStart": ""
"backToStart": "Zpět na začátek"
},
"user": {
"title": "Uživatelé",
"name": "Uživatel",
"page": {
"login": {
"title": "",
"subtitle": ""
"title": "Přihlaste se ke svému účtu",
"subtitle": "Vítejte zpět! Zadejte prosím Vaše přihlašovací údaje"
},
"invite": {
"title": "",
"subtitle": "",
"description": ""
"subtitle": "Vítejte v Homarru! Vytvořte si prosím svůj účet",
"description": "Byli jste pozváni {username}"
},
"init": {
"title": "",
"subtitle": ""
"title": "Nová instalace Homarru",
"subtitle": "Vytvořte prosím původního administrátora"
}
},
"field": {
@@ -133,45 +133,45 @@
"password": {
"label": "Heslo",
"requirement": {
"length": "",
"length": "Obsahuje alespoň 8 znaků",
"lowercase": "Obsahuje malé písmeno",
"uppercase": "Obsahuje velké písmeno",
"number": "Obsahuje číslo",
"special": ""
"special": "Obsahuje speciální symbol"
}
},
"passwordConfirm": {
"label": "Potvrďte heslo"
},
"previousPassword": {
"label": ""
"label": "Původní heslo"
},
"homeBoard": {
"label": ""
"label": "Domovská plocha"
},
"pingIconsEnabled": {
"label": ""
"label": "Používat ikony pro pingy"
}
},
"error": {
"usernameTaken": ""
"usernameTaken": "Uživatelské jméno je již používáno"
},
"action": {
"login": {
"label": "Přihlásit se",
"labelWith": "",
"labelWith": "Přihlaste se pomocí {provider}",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Přihlášení bylo úspěšné",
"message": "Nyní jste přihlášeni"
},
"error": {
"title": "",
"message": ""
"title": "Příhlášení selhalo",
"message": "Vaše přihlášení se nezdařilo"
}
},
"forgotPassword": {
"label": "",
"label": "Zapomněli jste Vaše heslo?",
"description": ""
}
},
@@ -180,40 +180,40 @@
"notification": {
"success": {
"title": "Účet byl vytvořen",
"message": ""
"message": "Chcete-li pokračovat, přihlaste se"
},
"error": {
"title": "",
"message": ""
"title": "Vytvoření účtu se nezdařilo",
"message": "Váš účet se nepodařilo vytvořit"
}
}
},
"create": "Vytvořit uživatele",
"changePassword": {
"label": "",
"label": "Změnit heslo",
"notification": {
"success": {
"message": ""
"message": "Heslo bylo úspěšně změněno"
},
"error": {
"message": ""
"message": "Nepodařilo se změnit heso"
}
}
},
"changeHomeBoard": {
"notification": {
"success": {
"message": ""
"message": "Domovská plocha byla úspěšně změněna"
},
"error": {
"message": ""
"message": "Nepodařilo se změnit domovskou plochu"
}
}
},
"changeFirstDayOfWeek": {
"notification": {
"success": {
"message": ""
"message": "První den v týdnu byl úspěšně změněn"
},
"error": {
"message": ""
@@ -247,14 +247,14 @@
}
},
"removeImage": {
"label": "",
"confirm": "",
"label": "Odebrat obrázek",
"confirm": "Opravdu chcete odstranit tento obrázek?",
"notification": {
"success": {
"message": ""
"message": "Obrázek byl úspěšně odstraněn"
},
"error": {
"message": ""
"message": "Obrázek nelze odstranit"
}
}
}
@@ -262,42 +262,42 @@
"editProfile": {
"notification": {
"success": {
"message": ""
"message": "Profil byl úspěšně aktualizován"
},
"error": {
"message": ""
"message": "Profil nelze aktualizovat"
}
}
},
"delete": {
"label": "",
"description": "",
"confirm": ""
"label": "Trvale smazat uživatele",
"description": "Smaže tohoto uživatele včetně jeho nastavení. Neodstraní žádné plochy. Uživatel nebude upozorněn.",
"confirm": "Jste si jisti, že chcete odstranit uživatele {username} včetně jejich nastavení?"
},
"select": {
"label": "",
"notFound": ""
"label": "Vybrat uživatele",
"notFound": "Nebyl nalezen žádný uživatel"
},
"transfer": {
"label": ""
"label": "Vyberte nového vlastníka"
}
}
},
"group": {
"title": "",
"name": "",
"search": "",
"title": "Skupiny",
"name": "Skupina",
"search": "Najít skupinu",
"field": {
"name": "Název",
"members": ""
"members": "Členové"
},
"permission": {
"admin": {
"title": "Administrátor",
"item": {
"admin": {
"label": "",
"description": ""
"label": "Administrátor",
"description": "Uživatelé s tímto oprávněním mají plný přístup ke všem funkcím a nastavením"
}
}
},
@@ -305,20 +305,20 @@
"title": "Aplikace",
"item": {
"create": {
"label": "",
"description": ""
"label": "Vytvořit aplikace",
"description": "Povolit členům vytvářet aplikace"
},
"use-all": {
"label": "",
"description": ""
"label": "Používat všechny aplikace",
"description": "Umožňuje členům přidávat do jejich ploch jakékoli aplikace"
},
"modify-all": {
"label": "",
"description": ""
"label": "Upravit všechny aplikace",
"description": "Umožňuje členům upravovat všechny aplikace"
},
"full-all": {
"label": "",
"description": ""
"label": "Úplný přístup k aplikacím",
"description": "Umožňuje členům spravovat, používat a mazat jakoukoli aplikaci"
}
}
},
@@ -326,16 +326,16 @@
"title": "Plochy",
"item": {
"create": {
"label": "",
"description": ""
"label": "Tvorba ploch",
"description": "Umožňuje členům vytvářet plochy"
},
"view-all": {
"label": "",
"description": ""
"label": "Zobrazit všechny plochy",
"description": "Umožňuje členům zobrazit všechny plochy"
},
"modify-all": {
"label": "",
"description": ""
"label": "Upravit všechny plochy",
"description": "Umožňuje členům upravovat všechny plochy (nezahrnuje kontrolu přístupu a nebezpečnou zónu)"
},
"full-all": {
"label": "",
@@ -344,83 +344,83 @@
}
},
"integration": {
"title": "",
"title": "Integrace",
"item": {
"create": {
"label": "",
"description": ""
"label": "Vytvořit integrace",
"description": "Povolit členům vytvářet integrace"
},
"use-all": {
"label": "",
"description": ""
"label": "Použít všechny integrace",
"description": "Umožňuje členům přidávat do jejich ploch jakékoli integrace"
},
"interact-all": {
"label": "",
"description": ""
"label": "Interakce s jakoukoli integrací",
"description": "Umožnit členům interagovat s jakoukoli integrací"
},
"full-all": {
"label": "",
"description": ""
"label": "Plný přístup k integraci",
"description": "Umožňuje členům spravovat, používat a interagovat s jakoukoli integrací"
}
}
},
"media": {
"title": "",
"title": "Média",
"item": {
"upload": {
"label": "",
"description": ""
"label": "Nahrávat média",
"description": "Povolit členům nahrávat média"
},
"view-all": {
"label": "",
"description": ""
"label": "Zobrazit všechna média",
"description": "Umožňuje členům nahrávat média"
},
"full-all": {
"label": "",
"label": "Úplný přístup k médiím",
"description": ""
}
}
},
"other": {
"title": "",
"title": "Ostatní",
"item": {
"view-logs": {
"label": "",
"description": ""
"label": "Zobrazit logy",
"description": "Umožňuje členům prohlížet logy"
}
}
},
"search-engine": {
"title": "",
"title": "Vyhledávače",
"item": {
"create": {
"label": "",
"description": ""
"label": "Vytvořit vyhledávače",
"description": "Umožňuje členům vytvářet vyhledávače"
},
"modify-all": {
"label": "",
"description": ""
"description": "Umožňuje členům upravovat všechny vyhledávače"
},
"full-all": {
"label": "",
"description": ""
"label": "Úplný přístup k vyhledávačům",
"description": "Umožňuje členům spravovat a mazat jakýkoliv vyhledávač"
}
}
}
},
"memberNotice": {
"mixed": "",
"external": ""
"mixed": "Někteří členové jsou od externích poskytovatelů a nelze je spravovat zde",
"external": "Všichni členové jsou od externích poskytovatelů a nelze je spravovat zde"
},
"reservedNotice": {
"message": ""
"message": "Tato skupina je vyhrazena pro používání systému a omezuje některé akce. <checkoutDocs></checkoutDocs>"
},
"action": {
"create": {
"label": "",
"label": "Nová skupina",
"notification": {
"success": {
"message": ""
"message": "Skupina byla úspěšně vytvořena"
},
"error": {
"message": ""
@@ -506,34 +506,34 @@
},
"error": {
"title": "",
"message": ""
"message": "Aplikaci nelze vytvořit"
}
}
},
"edit": {
"title": "",
"title": "Upravit aplikaci",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Změny úspěšně aplikovány",
"message": "Aplikace byla úspěšně uložena"
},
"error": {
"title": "",
"message": ""
"title": "Nepodařilo se uložit změny",
"message": "Aplikaci se nepodařilo uložit"
}
}
},
"delete": {
"title": "",
"message": "",
"title": "Vymazat aplikaci",
"message": "Jste si jisti, že chcete vymazat aplikaci {name}?",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Úspěšně odstraněno",
"message": "Aplikace byla úspěšně odstraněna"
},
"error": {
"title": "",
"message": ""
"title": "Smazání se nezdařilo",
"message": "Nelze odstranit aplikaci"
}
}
}
@@ -543,15 +543,15 @@
"label": "Název"
},
"description": {
"label": ""
"label": "Popis"
},
"url": {
"label": ""
"label": "Url"
}
},
"action": {
"select": {
"label": "",
"label": "Vybrat aplikaci",
"notFound": ""
}
}
@@ -559,49 +559,49 @@
"integration": {
"page": {
"list": {
"title": "",
"search": "",
"title": "Integrace",
"search": "Prohledat integrace",
"noResults": {
"title": ""
"title": "Zatím nejsou žádné integrace"
}
},
"create": {
"title": "",
"title": "Nová integrace {name}",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Vytvoření bylo úspěšné",
"message": "Integrace byla úspěšně vytvořena"
},
"error": {
"title": "",
"message": ""
"title": "Vytvoření se nezdařilo",
"message": "Integraci se nepodařilo vytvořit"
}
}
},
"edit": {
"title": "",
"title": "Upravit integraci {name}",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Změny byly úspěšně aplikovány",
"message": "Integrace byla úspěšně uložena"
},
"error": {
"title": "",
"message": ""
"title": "Nepodařilo se uložit změny",
"message": "Integraci se nepodařilo uložit"
}
}
},
"delete": {
"title": "",
"message": "",
"title": "Smazat integraci",
"message": "Jste si jisti, že chcete vymazat integraci {name}?",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Odstránění bylo úspěšné",
"message": "Integrace byla úspěšně smazána"
},
"error": {
"title": "",
"message": ""
"title": "Smazání se nezdařilo",
"message": "Nepodařilo se odstranit integraci"
}
}
}
@@ -611,41 +611,41 @@
"label": "Název"
},
"url": {
"label": ""
"label": "Url"
}
},
"action": {
"create": ""
"create": "Nová integrace"
},
"testConnection": {
"action": {
"create": "",
"edit": ""
"create": "Otestovat připojení a vytvořit",
"edit": "Otestovat připojení a uložit"
},
"alertNotice": "",
"notification": {
"success": {
"title": "",
"title": "Spojení bylo úspěšné",
"message": ""
},
"invalidUrl": {
"title": "Neplatná URL adresa",
"message": ""
"message": "URL adresa je neplatná"
},
"secretNotDefined": {
"title": "",
"message": ""
"title": "Chybějící přihlašovací údaje",
"message": "Nebyly poskytnuty všechny přihlašovací údaje"
},
"invalidCredentials": {
"title": "",
"message": ""
"title": "Neplatné přihlašovací údaje",
"message": "Přihlašovací údaje jsou neplatné"
},
"commonError": {
"title": "",
"message": ""
"title": "Spojení selhalo",
"message": "Spojení nelze navázat"
},
"badRequest": {
"title": "",
"title": "Chybný požadavek",
"message": ""
},
"unauthorized": {
@@ -766,89 +766,89 @@
}
},
"common": {
"beta": "",
"beta": "Beta",
"error": "Chyba",
"action": {
"add": "Přidat",
"apply": "Použít",
"backToOverview": "",
"backToOverview": "Zpět na přehled",
"create": "Vytvořit",
"edit": "Upravit",
"import": "",
"import": "Importovat",
"insert": "Vložit",
"remove": "Odstranit",
"save": "Uložit",
"saveChanges": "Uložit změny",
"cancel": "Zrušit",
"delete": "Odstranit",
"discard": "",
"discard": "Zahodit",
"confirm": "Potvrdit",
"continue": "",
"continue": "Pokračovat",
"previous": "Zpět",
"next": "Další",
"checkoutDocs": "",
"checkLogs": "",
"checkoutDocs": "Přečtěte si dokumentaci",
"checkLogs": "Pro více podrobností zkontrolujte logy",
"tryAgain": "Zkusit znovu",
"loading": ""
"loading": "Načítání"
},
"here": "",
"here": "zde",
"iconPicker": {
"label": "",
"label": "URL ikony",
"header": ""
},
"colorScheme": {
"options": {
"light": "",
"dark": ""
"light": "Světlý",
"dark": "Tmavý"
}
},
"information": {
"min": "",
"max": "",
"days": "",
"min": "Min",
"max": "Max",
"days": "Dnů",
"hours": "Hodin",
"minutes": "Minut"
},
"notification": {
"create": {
"success": "",
"error": ""
"success": "Vytvoření bylo úspěšné",
"error": "Vytvoření se nezdařilo"
},
"delete": {
"success": "",
"error": ""
"success": "Odstranění bylo úspěšné",
"error": "Odstranění se nezdařilo"
},
"update": {
"success": "",
"error": ""
"success": "Změny byly úspěšně aplikovány",
"error": "Nepodařilo se uložit změny"
},
"transfer": {
"success": "",
"error": ""
"success": "Přenos úspěšný",
"error": "Přenos se nezdařil"
}
},
"multiSelect": {
"placeholder": ""
},
"multiText": {
"placeholder": "",
"addLabel": ""
"placeholder": "Přidat více hodnot",
"addLabel": "Přidat {value}"
},
"select": {
"placeholder": "",
"placeholder": "Vybrat hodnotu",
"badge": {
"recommended": ""
"recommended": "Doporučeno"
}
},
"userAvatar": {
"menu": {
"switchToDarkMode": "",
"switchToLightMode": "",
"management": "",
"switchToDarkMode": "Přepnout do tmavého režimu",
"switchToLightMode": "Přepnout do světlého režimu",
"management": "Správa",
"preferences": "Vaše předvolby",
"logout": "",
"logout": "Odhlásit se",
"login": "Přihlásit se",
"homeBoard": "",
"homeBoard": "Vaše domácí plocha",
"loggedOut": "",
"updateAvailable": ""
}
@@ -856,8 +856,8 @@
"dangerZone": "Nebezpečná zóna",
"noResults": "Nebyly nalezeny žádné výsledky",
"preview": {
"show": "",
"hide": ""
"show": "Zobrazit náhled",
"hide": "Skrýt náhled"
},
"zod": {
"errors": {
@@ -867,7 +867,7 @@
"startsWith": "Toto pole musí začínat {startsWith}",
"endsWith": "Toto pole musí končit {endsWith}",
"includes": "Toto pole musí obsahovat {includes}",
"invalidEmail": ""
"invalidEmail": "Toto pole musí být platný e-mail"
},
"tooSmall": {
"string": "Toto pole musí obsahovat alespoň {minimum} znaků",
@@ -878,13 +878,13 @@
"number": "Toto pole musí být menší nebo rovno {maximum}"
},
"custom": {
"passwordsDoNotMatch": "",
"passwordRequirements": "",
"boardAlreadyExists": "",
"invalidFileType": "",
"fileTooLarge": "",
"invalidConfiguration": "",
"groupNameTaken": ""
"passwordsDoNotMatch": "Hesla se neshodují",
"passwordRequirements": "Heslo nesplňuje požadavky",
"boardAlreadyExists": "Plocha s tímto názvem již existuje",
"invalidFileType": "Neplatný typ souboru, očekáván {expected}",
"fileTooLarge": "Soubor je příliš velký, maximální velikost je {maxSize}",
"invalidConfiguration": "Neplatná konfigurace",
"groupNameTaken": "Název skupiny je již používán"
}
}
}
@@ -892,11 +892,11 @@
"section": {
"dynamic": {
"action": {
"create": "",
"remove": ""
"create": "Nová dynamická sekce",
"remove": "Odstranit dynamickou sekci"
},
"remove": {
"title": "",
"title": "Odstranit dynamickou sekci",
"message": ""
}
},
@@ -1047,7 +1047,7 @@
},
"dnsHoleSummary": {
"name": "",
"description": "",
"description": "Zobrazí shrnutí vaší DNS Hole",
"option": {
"layout": {
"label": "Rozložení",
@@ -1059,16 +1059,16 @@
"label": "Vertikální"
},
"grid": {
"label": ""
"label": "Mřížka"
}
}
},
"usePiHoleColors": {
"label": ""
"label": "Používat barvy Pi-Hole"
}
},
"error": {
"internalServerError": "",
"internalServerError": "Nepodařilo se načíst souhrn DNS Hole",
"integrationsDisconnected": ""
},
"data": {
@@ -1092,7 +1092,7 @@
"label": "Vertikální"
},
"grid": {
"label": ""
"label": "Mřížka"
}
}
},
@@ -1104,21 +1104,21 @@
"internalServerError": ""
},
"controls": {
"enableAll": "",
"disableAll": "",
"setTimer": "",
"enableAll": "Povolit vše",
"disableAll": "Zakázat vše",
"setTimer": "Nastavit časovač",
"set": "Nastavit",
"enabled": "Zapnuto",
"disabled": "Vypnuto",
"processing": "",
"processing": "Zpracovávání",
"disconnected": "",
"hours": "Hodin",
"minutes": "Minut",
"unlimited": ""
"unlimited": "Ponechte prázdné pro neomezené"
}
},
"clock": {
"name": "",
"name": "Datum a čas",
"description": "Zobrazuje aktuální datum a čas.",
"option": {
"customTitleToggle": {
@@ -1126,17 +1126,17 @@
"description": ""
},
"customTitle": {
"label": ""
"label": "Název"
},
"is24HourFormat": {
"label": "",
"description": ""
"label": "24hodinový formát",
"description": "Použít 24hodinový formát místo 12hodinového formátu"
},
"showSeconds": {
"label": ""
"label": "Zobrazit vteřiny"
},
"useCustomTimezone": {
"label": ""
"label": "Použít pevné časové pásmo"
},
"timezone": {
"label": "Časové pásmo",
@@ -1146,7 +1146,7 @@
"label": ""
},
"dateFormat": {
"label": "",
"label": "Formát data",
"description": ""
}
}
@@ -1213,11 +1213,11 @@
}
},
"iframe": {
"name": "",
"name": "iFrame",
"description": "Vložte jakýkoli obsah z internetu. Některé webové stránky mohou omezit přístup.",
"option": {
"embedUrl": {
"label": ""
"label": "Embed URL"
},
"allowFullScreen": {
"label": "Povolit celou obrazovku"
@@ -1245,12 +1245,12 @@
}
},
"error": {
"noUrl": "",
"noUrl": "Nebyla zadána URL adresa iFrame",
"noBrowerSupport": "Váš prohlížeč nepodporuje iFrame. Aktualizujte, prosím, svůj prohlížeč."
}
},
"smartHome-entityState": {
"name": "",
"name": "Stav entity",
"description": "",
"option": {
"entityId": {
@@ -1316,7 +1316,7 @@
"label": ""
},
"hasForecast": {
"label": ""
"label": "Zobrazit předpověď"
},
"forecastDayCount": {
"label": "",
@@ -1400,14 +1400,14 @@
"latitude": "",
"longitude": "",
"disabledTooltip": "",
"unknownLocation": "",
"unknownLocation": "Neznámá poloha",
"search": "Vyhledat",
"table": {
"header": {
"city": "",
"country": "",
"coordinates": "",
"population": ""
"city": "Město",
"country": "Státy",
"coordinates": "Souřadnice",
"population": "Populace"
},
"action": {
"select": ""
@@ -1418,16 +1418,16 @@
}
},
"integration": {
"noData": "",
"description": ""
"noData": "Nebyla nalezena žádná integrace",
"description": "Klikněte na <here></here> pro vytvoření nové integrace"
},
"app": {
"noData": "",
"description": ""
"description": "Klikněte na <here></here> pro vytvoření nové aplikace"
},
"error": {
"noIntegration": "",
"noData": ""
"noIntegration": "Nebyla vybrána žádná integrace",
"noData": "Nejsou k dispozici žádná data o integraci"
},
"option": {}
},
@@ -1440,7 +1440,7 @@
},
"hasAutoPlay": {
"label": "Automatické přehrávání",
"description": ""
"description": "Automatické přehrávání funguje pouze když je ztlumeno kvůli omezením prohlížeče"
},
"isMuted": {
"label": ""
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "Uživatel",
"name": "Jméno",
"id": "Id"
}
},
"downloads": {
"name": "",
@@ -1515,11 +1520,11 @@
"detailsTitle": "Rychlost stahování"
},
"index": {
"columnTitle": "",
"columnTitle": "#",
"detailsTitle": ""
},
"id": {
"columnTitle": ""
"columnTitle": "Id"
},
"integration": {
"columnTitle": "Integrace"
@@ -1536,12 +1541,12 @@
"detailsTitle": ""
},
"received": {
"columnTitle": "",
"detailsTitle": ""
"columnTitle": "Celkem staženo",
"detailsTitle": "Celkem staženo"
},
"sent": {
"columnTitle": "",
"detailsTitle": ""
"columnTitle": "Celkem nahráno",
"detailsTitle": "Celkem nahráno"
},
"size": {
"columnTitle": "",
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+7 -2
View File
@@ -1381,7 +1381,7 @@
"memory": "Speicher: {memory}GiB",
"memoryAvailable": "Verfügbar: {memoryAvailable} GiB ({percent}%)",
"version": "",
"uptime": "Betriebszeit: {days} Tage, {hours} Stunden, {minutes} Minuten",
"uptime": "Laufzeit: {months} Monate, {days} Tage, {hours} Stunden, {minutes} Minuten",
"loadAverage": "Durchschnittliche Last:",
"minute": "1 Minute",
"minutes": "{count} Minuten",
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+54 -2
View File
@@ -612,6 +612,10 @@
},
"url": {
"label": "Url"
},
"attemptSearchEngineCreation": {
"label": "Create Search Engine",
"description": "Integration \"{kind}\" can be used with the search engines. Check this to automatically configure the search engine."
}
},
"action": {
@@ -1151,6 +1155,25 @@
}
}
},
"minecraftServerStatus": {
"name": "Minecraft Server Status",
"description": "Displays the status of a Minecraft server",
"option": {
"title": {
"label": "Title"
},
"domain": {
"label": "Server address"
},
"isBedrockServer": {
"label": "Bedrock server"
}
},
"status": {
"online": "Online",
"offline": "Offline"
}
},
"notebook": {
"name": "Notebook",
"description": "A simple notebook widget that supports markdown",
@@ -1381,7 +1404,7 @@
"memory": "Memory: {memory}GiB",
"memoryAvailable": "Available: {memoryAvailable}GiB ({percent}%)",
"version": "Version: {version}",
"uptime": "Uptime: {days} Days, {hours} Hours, {minutes} Minutes",
"uptime": "Uptime: {months} Months, {days} Days, {hours} Hours, {minutes} Minutes",
"loadAverage": "Load average:",
"minute": "1 minute",
"minutes": "{count} minutes",
@@ -1457,7 +1480,12 @@
"mediaServer": {
"name": "Current media server streams",
"description": "Show the current streams on your media servers",
"option": {}
"option": {},
"items": {
"user": "User",
"name": "Name",
"id": "Id"
}
},
"downloads": {
"name": "Download Client",
@@ -2319,6 +2347,9 @@
"error": "Error"
},
"job": {
"minecraftServerStatus": {
"label": "Minecraft server status"
},
"iconsUpdater": {
"label": "Icons Updater"
},
@@ -2717,6 +2748,11 @@
}
}
},
"media": {
"requestMovie": "Request movie",
"requestSeries": "Request series",
"openIn": "Open in {kind}"
},
"external": {
"help": "Use an external search engine",
"group": {
@@ -2957,6 +2993,22 @@
}
}
}
},
"media": {
"request": {
"modal": {
"title": "Request \"{name}\"",
"table": {
"header": {
"season": "Season",
"episodes": "Episodes"
}
},
"button": {
"send": "Send request"
}
}
}
}
}
}
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
File diff suppressed because it is too large. Load diff
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+54 -49
View File
@@ -323,14 +323,14 @@
}
},
"board": {
"title": "",
"title": "Board",
"item": {
"create": {
"label": "Creare schede",
"description": "Consenti ai membri di creare schede"
"label": "Creare board",
"description": "Consenti ai membri di creare board"
},
"view-all": {
"label": "Visualizzare tutte le schede",
"label": "Visualizzare tutte le board",
"description": "Consenti ai membri di visualizzare tutte le schede"
},
"modify-all": {
@@ -402,50 +402,50 @@
"description": "Consenti ai membri di modificare tutti i motori di ricerca"
},
"full-all": {
"label": "",
"description": ""
"label": "Accesso completo al motore di ricerca",
"description": "Consenti ai membri di gestire ed eliminare qualsiasi motore di ricerca"
}
}
}
},
"memberNotice": {
"mixed": "",
"external": ""
"mixed": "Alcuni membri provengono da fornitori esterni e non possono essere gestiti qui",
"external": "Tutti i membri provengono da fornitori esterni e non possono essere gestiti qui"
},
"reservedNotice": {
"message": ""
"message": "Questo gruppo è riservato all'uso del sistema e limita alcune azioni. <checkoutDocs></checkoutDocs>"
},
"action": {
"create": {
"label": "",
"label": "Nuovo gruppo",
"notification": {
"success": {
"message": ""
"message": "Gruppo creato"
},
"error": {
"message": ""
"message": "Impossibile creare il gruppo"
}
}
},
"transfer": {
"label": "",
"description": "",
"confirm": "",
"label": "Trasferisci ownership",
"description": "Trasferisci l'ownership di questo gruppo a un altro utente.",
"confirm": "Hai la certezza di voler trasferire l'ownership del gruppo {name} a {username}?",
"notification": {
"success": {
"message": ""
"message": "Gruppo {group} trasferito a {user}"
},
"error": {
"message": ""
"message": "Impossibile trasferire l'ownership"
}
}
},
"addMember": {
"label": ""
"label": "Aggiungi membro"
},
"removeMember": {
"label": "",
"confirm": ""
"label": "Rimuovi membro",
"confirm": "Hai la certezza di voler rimuovere {user} da questo gruppo?"
},
"delete": {
"label": "Elimina gruppo",
@@ -506,34 +506,34 @@
},
"error": {
"title": "Creazione fallita",
"message": ""
"message": "Impossibile creare l'app"
}
}
},
"edit": {
"title": "",
"title": "Modifica app",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Modifiche applicate",
"message": "App salvata"
},
"error": {
"title": "",
"message": ""
"title": "Impossibile applicare le modifiche",
"message": "Impossibile salvare l'app"
}
}
},
"delete": {
"title": "",
"message": "",
"title": "Elimina app",
"message": "Hai la certezza di voler eliminare l'app {name}?",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Eliminato",
"message": "App eliminata"
},
"error": {
"title": "",
"message": ""
"title": "Eliminazione non riuscita",
"message": "Impossibile eliminare l'app"
}
}
}
@@ -543,15 +543,15 @@
"label": "Nome"
},
"description": {
"label": ""
"label": "Descrizione"
},
"url": {
"label": ""
"label": "Url"
}
},
"action": {
"select": {
"label": "",
"label": "Seleziona App",
"notFound": "Nessuna app trovata"
}
}
@@ -569,11 +569,11 @@
"title": "Nuova integrazione di {name}",
"notification": {
"success": {
"title": "",
"title": "Creazione riuscita",
"message": "L'integrazione è stata creata con successo"
},
"error": {
"title": "",
"title": "Creazione fallita",
"message": "Non è stato possibile creare l'integrazione"
}
}
@@ -582,11 +582,11 @@
"title": "Modifica integrazione {name}",
"notification": {
"success": {
"title": "",
"title": "Modifiche applicate",
"message": "L'integrazione è stata salvata correttamente"
},
"error": {
"title": "",
"title": "Impossibile applicare le modifiche",
"message": "Non è stato possibile salvare l'integrazione"
}
}
@@ -596,11 +596,11 @@
"message": "Hai la certezza di voler eliminare l'integrazione {name}?",
"notification": {
"success": {
"title": "",
"title": "Eliminato",
"message": "L'integrazione è stata eliminata correttamente"
},
"error": {
"title": "",
"title": "Eliminazione non riuscita",
"message": "Impossibile eliminare l'integrazione"
}
}
@@ -611,7 +611,7 @@
"label": "Nome"
},
"url": {
"label": ""
"label": "Url"
}
},
"action": {
@@ -645,7 +645,7 @@
"message": "La connessione non è stata stabilita"
},
"badRequest": {
"title": "",
"title": "Bad request",
"message": "La richiesta era malformata"
},
"unauthorized": {
@@ -653,11 +653,11 @@
"message": "Probabilmente le credenziali sono sbagliate"
},
"forbidden": {
"title": "",
"title": "Forbidden",
"message": "Probabilmente mancano i permessi"
},
"notFound": {
"title": "",
"title": "Not found",
"message": "Probabilmente url o percorso errati"
},
"internalServerError": {
@@ -691,11 +691,11 @@
}
},
"secrets": {
"title": "",
"title": "Secrets",
"lastUpdated": "Ultimo aggiornamento {date}",
"notSet": {
"label": "",
"tooltip": ""
"label": "Nessun valore impostato",
"tooltip": "Il secret richiesto non è stato ancora impostato"
},
"secureNotice": "",
"reset": {
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+9 -4
View File
@@ -2,16 +2,16 @@
"init": {
"step": {
"start": {
"title": "",
"title": "Homarrへようこそ",
"subtitle": "",
"description": "",
"action": {
"scratch": "",
"scratch": "ゼロからスタート",
"importOldmarr": ""
}
},
"import": {
"title": "",
"title": "データのインポート",
"subtitle": "",
"dropzone": {
"title": "",
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
File diff suppressed because it is too large. Load diff
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+7 -2
View File
@@ -1254,7 +1254,7 @@
"description": "",
"option": {
"entityId": {
"label": "ID encji"
"label": "ID obiektu"
},
"displayName": {
"label": ""
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+7 -2
View File
@@ -1381,7 +1381,7 @@
"memory": "Память: {memory}GiB",
"memoryAvailable": "Доступно: {memoryAvailable}GiB ({percent}%)",
"version": "Версия: {version}",
"uptime": "Время работы: {days} дн., {hours} ч., {minutes} мин.",
"uptime": "Время работы: {months} мес., {days} дн., {hours} ч., {minutes} мин.",
"loadAverage": "Средняя нагрузка:",
"minute": "1 минута",
"minutes": "{count} минут",
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "Активные просмотры",
"description": "Отображает текущие сеансы воспроизведения на медиасерверах",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "Загрузки",
+9 -4
View File
@@ -2,9 +2,9 @@
"init": {
"step": {
"start": {
"title": "Vitajte na stránke Homarr",
"subtitle": "Začnime s nastavením inštancie Homarr.",
"description": "Ak chcete začať, vyberte, ako chcete nastaviť inštanciu Homarr.",
"title": "Vitajte na Homár",
"subtitle": "Začnime s nastavením inštancie Homár.",
"description": "Ak chcete začať, vyberte, ako chcete nastaviť inštanciu Homár.",
"action": {
"scratch": "Začať odznova",
"importOldmarr": "Import z Homarru pred 1.0"
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "Aktuálne streamy mediálneho servera",
"description": "Zobrazte aktuálne streamy na vašich mediálnych serveroch",
"option": {}
"option": {},
"items": {
"user": "Používateľ",
"name": "Názov",
"id": "Id"
}
},
"downloads": {
"name": "Download klient",
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+123 -118
View File
@@ -147,7 +147,7 @@
"label": "Eski Şifre"
},
"homeBoard": {
"label": "Varsayılan Panel"
"label": "Öntanımlı panel"
},
"pingIconsEnabled": {
"label": "Pingler İçin Simgeler Kullanın"
@@ -203,10 +203,10 @@
"changeHomeBoard": {
"notification": {
"success": {
"message": "Varsayılan panel başarıyla değiştirildi"
"message": "Öntanımlı panel başarıyla değiştirildi"
},
"error": {
"message": "Varsayılan panel değiştirilemiyor"
"message": "Öntanımlı panel değiştirilemiyor"
}
}
},
@@ -498,7 +498,7 @@
}
},
"create": {
"title": "Yeni uygulama",
"title": "Yeni Uygulama",
"notification": {
"success": {
"title": "Oluşturma başarılı",
@@ -566,7 +566,7 @@
}
},
"create": {
"title": "Yeni {name} entegrasyonu",
"title": "Yeni {name} Entegrasyonu",
"notification": {
"success": {
"title": "Oluşturma başarılı",
@@ -615,7 +615,7 @@
}
},
"action": {
"create": "Yeni entegrasyon"
"create": "Yeni Entegrasyon"
},
"testConnection": {
"action": {
@@ -703,13 +703,13 @@
"message": "Gizli anahtarı sıfırlamak istediğinizden emin misiniz?"
},
"noSecretsRequired": {
"segmentTitle": "Hiçbir Gizli anahtar yok",
"segmentTitle": "Gizli Anahtar Yok",
"text": "Bu entegrasyon için gizli anahtar gerekmiyor"
},
"kind": {
"username": {
"label": "Kullanıcı adı",
"newLabel": "Yeni kullanıcı adı"
"newLabel": "Yeni Kullanıcı Adı"
},
"apiKey": {
"label": "API Anahtarı",
@@ -717,7 +717,7 @@
},
"password": {
"label": "Şifre",
"newLabel": "Yeni parola"
"newLabel": "Yeni Şifre"
}
}
},
@@ -842,18 +842,18 @@
},
"userAvatar": {
"menu": {
"switchToDarkMode": "Koyu moda geç",
"switchToLightMode": "Aydınlık moda geç",
"switchToDarkMode": "Koyu Temaya Geç",
"switchToLightMode": "Aydınlık Temaya Geç",
"management": "Yönetim",
"preferences": "Tercihleriniz",
"logout": "Oturumu kapat",
"login": "Giriş",
"homeBoard": "Varsayılan Panel",
"homeBoard": "Öntanımlı Panel",
"loggedOut": "Oturum kapatıldı",
"updateAvailable": "{countUpdates} güncelleme mevcut: {tag}"
}
},
"dangerZone": "Tehlikeli bölge",
"dangerZone": "Tehlikeli Bölge",
"noResults": "Sonuç bulunamadı",
"preview": {
"show": "Ara Renkleri Göster",
@@ -892,7 +892,7 @@
"section": {
"dynamic": {
"action": {
"create": "Yeni dinamik bölüm",
"create": "Yeni Dinamik Bölüm",
"remove": "Dinamik bölümü kaldır"
},
"remove": {
@@ -907,16 +907,16 @@
}
},
"action": {
"create": "Yeni kategori",
"edit": "Kategoriyi yeniden adlandır",
"create": "Yeni Kategori",
"edit": "Kategoriyi Yeniden Adlandır",
"remove": "Kategoriyi kaldır",
"moveUp": "Yukarı taşı",
"moveDown": "Aşağı taşı",
"createAbove": "Yukarı yeni kategori",
"createBelow": "Aşağı yeni kategori"
"createAbove": "Yukarı Yeni Kategori",
"createBelow": "Aşağı Yeni Kategori"
},
"create": {
"title": "Yeni kategori",
"title": "Yeni Kategori",
"submit": "Kategori ekle"
},
"remove": {
@@ -924,12 +924,12 @@
"message": "{name} kategorisini kaldırmak istediğinizden emin misiniz?"
},
"edit": {
"title": "Kategoriyi yeniden adlandır",
"submit": "Kategoriyi yeniden adlandır"
"title": "Kategoriyi Yeniden Adlandır",
"submit": "Kategoriyi Yeniden Adlandır"
},
"menu": {
"label": {
"create": "Yeni kategori",
"create": "Yeni Kategori",
"changePosition": "Pozisyonu değiştir"
}
}
@@ -937,10 +937,10 @@
},
"item": {
"action": {
"create": "Yeni öğe",
"create": "Yeni Öğe",
"import": "Öğeleri İçe Aktar",
"edit": "Öğeyi Düzenle",
"moveResize": "Öğeyi taşı / yeniden boyutlandır",
"moveResize": "Öğeyi Taşı / Yeniden Boyutlandır",
"duplicate": "Öğeyi Çoğalt",
"remove": "Öğeyi kaldır"
},
@@ -954,7 +954,7 @@
"addToBoard": "Panele Ekle"
},
"moveResize": {
"title": "Öğeyi taşı / yeniden boyutlandır",
"title": "Öğeyi Taşı / Yeniden Boyutlandır",
"field": {
"width": {
"label": "Genişlik"
@@ -981,7 +981,7 @@
"label": "Entegrasyonlar"
},
"customCssClasses": {
"label": "Özel css sınıfları"
"label": "Özel CSS Alanı"
}
}
},
@@ -999,7 +999,7 @@
"label": "Uygulama seç"
},
"openInNewTab": {
"label": "Yeni sekmede aç"
"label": "Yeni Sekmede Aç"
},
"showTitle": {
"label": "Uygulama adını göster"
@@ -1013,13 +1013,13 @@
},
"error": {
"notFound": {
"label": "Uygulama yok",
"label": "Uygulama Eklenmedi",
"tooltip": "Geçerli bir uygulama seçmediniz"
}
}
},
"bookmarks": {
"name": "Yer imleri",
"name": "Yer İmleri",
"description": "Birden fazla uygulama bağlantısını görüntüler",
"option": {
"title": {
@@ -1040,7 +1040,7 @@
}
},
"items": {
"label": "Yer imleri",
"label": "Yer İmleri",
"add": "Yer imi ekle"
}
}
@@ -1069,7 +1069,7 @@
},
"error": {
"internalServerError": "DNS Çözümleyici Özeti alınamadı",
"integrationsDisconnected": "Veri yok, tüm entegrasyonların bağlantısı kesildi"
"integrationsDisconnected": "Veri Bulunamadı, Tüm Entegrasyonların Bağlantısı Kesildi"
},
"data": {
"adsBlockedToday": "Bugün engellenenler",
@@ -1079,7 +1079,7 @@
}
},
"dnsHoleControls": {
"name": "DNS çözümleyici kontrolleri",
"name": "DNS Çözümleyici Kontrolleri",
"description": "Kontrol panelinizden PiHole veya AdGuard'ı kontrol edin",
"option": {
"layout": {
@@ -1118,7 +1118,7 @@
}
},
"clock": {
"name": "Tarih ve saat",
"name": "Tarih ve Saat",
"description": "Geçerli tarih ve saati görüntüler.",
"option": {
"customTitleToggle": {
@@ -1152,7 +1152,7 @@
}
},
"notebook": {
"name": "Not defteri",
"name": "Not Defteri",
"description": "Markdown'ı destekleyen basit bir not defteri bileşeni",
"option": {
"showToolbar": {
@@ -1268,7 +1268,7 @@
}
},
"smartHome-executeAutomation": {
"name": "Otomasyonu Yürüt",
"name": "Otomasyon Yürüt",
"description": "Tek tıklamayla otomasyonu tetikleyin",
"option": {
"displayName": {
@@ -1345,17 +1345,17 @@
}
},
"indexerManager": {
"name": "Dizin oluşturucu yöneticisi statüsü",
"description": "Dizinleyicilerinizin durumu",
"name": "İndeksleyici Yönetim Durumu",
"description": "İndeksleyicilerinizin durumu",
"option": {
"openIndexerSiteInNewTab": {
"label": "Dizinleyici sitesini yeni sekmede aç"
"label": "İndeksleyici Sitesini Yeni Sekmede Aç"
}
},
"title": "Dizin oluşturucu yöneticisi",
"title": "İndeksleyici Yöneticisi",
"testAll": "Tümünü test et",
"error": {
"internalServerError": "Dizinleyicilerin durumu alınamadı"
"internalServerError": "İndeksleyicilerin durumu alınamadı"
}
},
"healthMonitoring": {
@@ -1379,14 +1379,14 @@
"information": "Bilgi",
"processor": "İşlemci: {cpuModelName}",
"memory": "Bellek: {memory}GiB",
"memoryAvailable": "Mevcut: {memoryAvailable}GiB ({percent}%)",
"memoryAvailable": "Kullanılabilir: {memoryAvailable} GiB ({percent}%)",
"version": "Sürüm: {version}",
"uptime": "Çalışma Süresi: {days} Gün, {hours} Saat, {minutes} Dakika",
"loadAverage": "Ortalama yük:",
"minute": "1 dakika",
"minutes": "{count} dakika",
"used": "Kullanılan",
"available": "Mevcut",
"available": "Kullanılabilir",
"lastSeen": "Son durum güncellemesi: {lastSeen}"
},
"memory": {},
@@ -1455,9 +1455,14 @@
}
},
"mediaServer": {
"name": "Güncel medya sunucusu akışları",
"name": "Güncel Medya Sunucusu Akışları",
"description": "Medya sunucularınızdaki mevcut akışları gösterin",
"option": {}
"option": {},
"items": {
"user": "Kullanıcı",
"name": "İsim",
"id": "Kimlik"
}
},
"downloads": {
"name": "İndirme İstemcisi",
@@ -1470,7 +1475,7 @@
"label": "Öğelerin sıralanmasını etkinleştir"
},
"defaultSort": {
"label": "Varsayılan olarak sıralama için kullanılan sütun"
"label": "Öntanımlı olarak sıralama için kullanılan sütun"
},
"descendingDefaultSort": {
"label": "Ters sıralama"
@@ -1644,14 +1649,14 @@
}
},
"mediaTranscoding": {
"name": "Medya kod dönüştürme",
"name": "Medya Kod Dönüştürme",
"description": "Medya kod dönüştürmenizin istatistikleri, geçerli kuyruğu ve çalışan durumu",
"option": {
"defaultView": {
"label": "Varsayılan görünüm"
"label": "Öntanımlı görünüm"
},
"queuePageSize": {
"label": "Sıra sayfa boyutu"
"label": "Kuyruk Sayfa Boyutu"
}
},
"tab": {
@@ -1703,8 +1708,8 @@
}
},
"rssFeed": {
"name": "RSS beslemeleri",
"description": "Bir veya daha fazla genel RSS, ATOM veya JSON beslemesini izleyin ve görüntüleyin",
"name": "RSS Beslemeleri",
"description": "Bir veya daha fazla RSS, ATOM veya JSON beslemesini takip edin ve görüntüleyin",
"option": {
"feedUrls": {
"label": "Besleme URL'leri"
@@ -1807,22 +1812,22 @@
},
"field": {
"pageTitle": {
"label": "Sayfa başlığı"
"label": "Sayfa Başlığı"
},
"metaTitle": {
"label": "Meta başlığı"
"label": "Meta Başlığı"
},
"logoImageUrl": {
"label": "Logo görsel URL'si"
"label": "Logo Görseli URL'si"
},
"faviconImageUrl": {
"label": "Favicon görsel URL'si"
"label": "Favicon Görseli URL'si"
},
"backgroundImageUrl": {
"label": "Arkaplan görsel URL'si"
"label": "Arkaplan Resmi URL'si"
},
"backgroundImageAttachment": {
"label": "Arkaplan görsel yerleştirme",
"label": "Arka Plan Yerleşimi",
"option": {
"fixed": {
"label": "Sabit",
@@ -1830,19 +1835,19 @@
},
"scroll": {
"label": "Kaydır",
"description": "Arkaplan farenizle birlikte kayar."
"description": "Arkaplan Farenizle Birlikte Kayar."
}
}
},
"backgroundImageRepeat": {
"label": "Arkaplan görüntüsünü Tekrarla",
"label": "Arkaplan Resmini Tekrarla",
"option": {
"repeat": {
"label": "Tekrarla",
"description": "Görsel, tüm arka plan alanını kaplayacak kadar tekrarlanır."
},
"no-repeat": {
"label": "Tekrarlama yok",
"label": "Tekrarlama Yok",
"description": "Görsel tekrarlanmaz ve tüm alanı doldurmayabilir."
},
"repeat-x": {
@@ -1856,7 +1861,7 @@
}
},
"backgroundImageSize": {
"label": "Arkaplan görsel boyutu",
"label": "Arkaplan Resim Boyutu",
"option": {
"cover": {
"label": "Kapak",
@@ -1879,14 +1884,14 @@
},
"customCss": {
"label": "Bu panel için özel css",
"description": "Kontrol panelinizi yalnızca deneyimli kullanıcılar için önerilen CSS kullanarak özelleştirin",
"description": "CSS kullanarak panelizi özelleştirin, sadece tecrübeli kullanıcılar için tavsiye edilir",
"customClassesAlert": {
"title": "Özel sınıflar",
"description": "Her öğenin gelişmiş seçeneklerinde panel örneklerinize özel sınıflar ekleyebilir ve bunları yukarıdaki özel CSS'de kullanabilirsiniz."
"description": "Düzenlenen her aracın gelişmiş seçenekler sekmesinde bulunan alanı kullanarak panel öğelerinizi CSS ile özelleştirebilir ve ya yukarıdaki özel CSS alanını kullanabilirsiniz."
}
},
"columnCount": {
"label": "Sütun sayısı"
"label": "Sütun Sayısı"
},
"name": {
"label": "İsim"
@@ -1919,7 +1924,7 @@
"title": "Özel Css"
},
"access": {
"title": "Erişim kontrolü",
"title": "Erişim Kontrolü",
"permission": {
"item": {
"view": {
@@ -1935,14 +1940,14 @@
}
},
"dangerZone": {
"title": "Tehlikeli bölge",
"title": "Tehlikeli Bölge",
"action": {
"rename": {
"label": "Panelin adını değiştir",
"label": "Panelin ismini değiştir",
"description": "İsmin değiştirilmesi bu panele olan tüm bağlantıların kopmasına neden olacaktır.",
"button": "İsmi değiştir",
"modal": {
"title": "Panelin adını değiştir"
"title": "Panelin ismini değiştir"
}
},
"visibility": {
@@ -1993,7 +1998,7 @@
"notice": "Bağlantıyı kontrol edin veya erişilebilir olması gerektiğini düşünüyorsanız bir yöneticiyle iletişime geçin"
},
"homeBoard": {
"title": "Ana panel yok",
"title": "Öntanımlı Panel Yapılandırılmadı",
"admin": {
"description": "Sunucu için henüz bir ana sayfa paneli belirlemediniz.",
"link": "Sunucu geneli için ana panel yapılandırın",
@@ -2001,13 +2006,13 @@
},
"user": {
"description": "Henüz bir ev paneli belirlemediniz.",
"link": "Varsayılan Paneli Yapılandır",
"notice": "Bu Sayfanın Kaybolmasını Sağlamak İçin Tercihlerden Varsayılan Panel Atayın"
"link": "Öntanımlı paneli yapılandır",
"notice": "Bu sayfanın kaybolmasını sağlamak için tercihlerden öntanımlı panel seçin"
},
"anonymous": {
"description": "Sunucu Yöneticiniz Henüz Bir Varsayılan Panel Atamadı.",
"description": "Sunucu yöneticiniz henüz bir öntanımlı panel belirlemedi.",
"link": "Herkese açık panelleri görüntüle",
"notice": "Bu Sayfanın Kaybolmasını Sağlamak İçin Sunucu Yöneticisinden Varsayılan Panel Atamasını isteyin"
"notice": "Bu sayfanın kaybolmasını sağlamak için sunucu yöneticisinden öntanımlı panel ayarlamasını isteyin"
}
}
}
@@ -2082,7 +2087,7 @@
"title": "Panelleriniz",
"action": {
"new": {
"label": "Yeni panel"
"label": "Yeni Panel"
},
"open": {
"label": "Panel'i aç"
@@ -2091,10 +2096,10 @@
"label": "Ayarlar"
},
"setHomeBoard": {
"label": "Varsayılan Panel Olarak Ata",
"label": "Öntanımlı panel olarak ayarlayın",
"badge": {
"label": "Ana Sayfa",
"tooltip": "Bu panel sizin varsayılan paneliniz olarak görüntülenecektir"
"tooltip": "Bu panel sizin öntanımlı paneliniz olarak görüntülenecektir"
}
},
"delete": {
@@ -2130,7 +2135,7 @@
"title": "Genel",
"item": {
"language": "Dil & Bölge",
"board": "Varsayılan Panel",
"board": "Öntanımlı panel",
"firstDayOfWeek": "Haftanın ilk günü",
"accessibility": "Erişilebilirlik"
}
@@ -2151,7 +2156,7 @@
},
"create": {
"metaTitle": "Kullanıcı ekle",
"title": "Yeni kullanıcı oluştur",
"title": "Yeni Kullanıcı Oluştur",
"step": {
"personalInformation": {
"label": "Kişisel bilgiler"
@@ -2183,7 +2188,7 @@
"title": "Kullanıcı Davetlerini Yönet",
"action": {
"new": {
"title": "Yeni davet",
"title": "Yeni Davet",
"description": "Süre sona erdikten sonra davet artık geçerli olmayacak ve daveti alan kişi bir hesap oluşturamayacaktır."
},
"copy": {
@@ -2254,11 +2259,11 @@
},
"widgetData": {
"title": "Widget verileri",
"text": "Widget (ve sayıları) yapılandırma verilerini gönder. URL'leri, adları veya başka herhangi bir veriyi içermez."
"text": "Araç (ve sayılarını) yapılandırma verilerini gönder. URL'leri, alan adlarını veya başka herhangi bir veriyi içermez."
},
"integrationData": {
"title": "Entegrasyon verileri",
"text": "Entegrasyon (ve sayıları) yapılandırma verilerini gönder. URL'leri, adları veya başka herhangi bir veriyi içermez."
"text": "Entegrasyon (ve sayıları) yapılandırma verilerini gönder. URL'leri, alan adlarını veya başka herhangi bir veriyi içermez."
},
"usersData": {
"title": "Kullanıcı verileri",
@@ -2266,36 +2271,36 @@
}
},
"crawlingAndIndexing": {
"title": "Tarama ve Dizinleme",
"warning": "Burada herhangi bir ayarı etkinleştirmek veya devre dışı bırakmak, arama motorlarının sayfanızı nasıl dizine ekleyeceğini ve tarayacağını ciddi şekilde etkileyecektir. Herhangi bir ayar bir istektir ve bu ayarları uygulamak tarayıcıya bağlıdır. Herhangi bir değişikliğin uygulanması birkaç gün veya hafta sürebilir. Bazı ayarlar arama motoruna özgü olabilir.",
"title": "Arama ve İndeksleme",
"warning": "Buradaki ayarların etkinleştirilmesi veya devre dışı bırakılması, arama motorlarının sayfanızı arama ve indekleme şeklini ciddi şekilde etkileyecektir. Her ayar bir istek başlatır ve bu ayarları almak tarayıcının görevidir. Değişiklikler birkaç gün veya hafta sürebilir. Bazı ayarlar arama motoruna özel olabilir.",
"noIndex": {
"title": "İndeks yok",
"text": "Web sitesini arama motorlarında indekslemeyin ve hiçbir arama sonucunda göstermeyin"
"title": "İndeksleme Yapma",
"text": "Web sitesini arama motorlarında indekslemeyin veya herhangi bir arama sonucunda görüntülemeyin"
},
"noFollow": {
"title": "Takip yok",
"text": "Dizinleme sırasında hiçbir bağlantıyı takip etmeyin. Bunu devre dışı bırakmak, tarayıcıların Homarr'daki tüm bağlantıları takip etmeye çalışmasına yol açacaktır."
"title": "Bağlantıları Takip Etme",
"text": "İndeksleme sırasında bağlantıları takip etmeyin. Bunu devre dışı bırakmak, tarayıcıların Homarr'daki tüm bağlantıları izlemeye çalışmasına neden olur."
},
"noTranslate": {
"title": "Çeviri yok",
"text": "Site dilinin kullanıcının okumak isteyeceği bir dil olmaması durumunda Google, arama sonuçlarında bir çeviri bağlantısı gösterecektir"
"title": "Çeviri Yapma",
"text": "Kullanıcının site üzerinde okumak isteyeceği dilin olmaması durumunda Google, arama sonuçlarında bir çeviri bağlantısı gösterecektir"
},
"noSiteLinksSearchBox": {
"title": "Site bağlantıları arama kutusu yok",
"text": "Google, taranan bağlantıların yanı sıra diğer doğrudan bağlantılarla bir arama kutusu oluşturacaktır. Bunu etkinleştirmek Google'dan bu kutuyu devre dışı bırakmasını isteyecektir."
"title": "Site Arama Alanını Gösterme",
"text": "Google, aranan bağlantıların yanı sıra diğer doğrudan bağlantılardan oluşan bir arama kutusu oluşturur. Bu seçenek etkinleştirildiğinde Google'dan bu alanı devre dışı bırakması istenecektir."
}
},
"board": {
"title": "Paneller",
"homeBoard": {
"label": "Genel Varsayılan Panel",
"description": "Seçim için yalnızca herkese açık paneller kullanılabilir"
"label": "Genel Ana Sayfa Paneli",
"description": "Seçim yalnızca herkese açık paneller için kullanılabilir"
}
},
"appearance": {
"title": "Görünüş",
"defaultColorScheme": {
"label": "Varsayılan renk şeması",
"label": "Öntanımlı Renk Düzeni",
"options": {
"light": "Aydınlık",
"dark": "Koyu"
@@ -2305,7 +2310,7 @@
"culture": {
"title": "Kültür",
"defaultLocale": {
"label": "Varsayılan dil"
"label": "Öntanımlı Dil"
}
}
}
@@ -2320,7 +2325,7 @@
},
"job": {
"iconsUpdater": {
"label": "Simge Güncelleyici"
"label": "Simge Güncelleme"
},
"analytics": {
"label": "Analiz"
@@ -2347,10 +2352,10 @@
"label": "Medya Talep Listesi"
},
"rssFeeds": {
"label": "RSS beslemeleri"
"label": "RSS Beslemeleri"
},
"indexerManager": {
"label": "Dizinleyici Yöneticisi"
"label": "İndeksleyici Yöneticisi"
},
"healthMonitoring": {
"label": "Sağlık İzleme"
@@ -2365,7 +2370,7 @@
"label": "Güncelleme denetleyicisi"
},
"mediaTranscoding": {
"label": "Medya kod dönüştürme"
"label": "Medya Kod Dönüştürme"
}
}
},
@@ -2400,7 +2405,7 @@
},
"about": {
"version": "Sürüm {version}",
"text": "Homarr, gönüllüler tarafından sürdürülen topluluk odaklı bir açık kaynak projesidir. Bu kişiler sayesinde Homarr, 2021'den beri büyüyen bir projedir. Ekibimiz, boş zamanlarında Homarr üzerinde birçok farklı ülkeden tamamen uzaktan, hiçbir ücret almadan çalışmaktadır.",
"text": "Homarr, gönüllüler tarafından sürdürülen topluluk odaklı bir açık kaynak projesidir. Bu kişiler sayesinde Homarr, 2021'den bugüne büyüyerek ilerleyen bir projedir. Ekibimiz, boş zamanlarında Homarr üzerinde birçok farklı ülkeden tamamen uzaktan, hiçbir ücret almadan çalışmaktadır.",
"accordion": {
"contributors": {
"title": "Katkıda Bulunanlar",
@@ -2408,7 +2413,7 @@
},
"translators": {
"title": "Çevirmenler",
"subtitle": "{count} dilde birçok kişi çevirilere katkıda bulunuyor"
"subtitle": "Birçok kişi Homarr'ın {count} dile çevrilmesine katkı sağlıyor"
},
"libraries": {
"title": "Kütüphaneler",
@@ -2435,7 +2440,7 @@
"created": "Oluşturuldu",
"running": "Çalışıyor",
"paused": "Duraklatıldı",
"restarting": "Yeniden başlatılıyor",
"restarting": "Yeniden Başlatılıyor",
"exited": "Çıkıldı",
"removing": "Kaldırılıyor",
"dead": "Ölü"
@@ -2564,7 +2569,7 @@
}
},
"search-engines": {
"label": "Arama motorları",
"label": "Arama Motorları",
"new": {
"label": "Yeni"
},
@@ -2617,7 +2622,7 @@
}
},
"search": {
"placeholder": "Ara",
"placeholder": "Ara (\"!\" İle Başlamalı)",
"nothingFound": "Hiçbir şey bulunamadı",
"error": {
"fetch": "Veriler alınırken bir hata oluştu"
@@ -2650,7 +2655,7 @@
"label": "Panel'i aç"
},
"homeBoard": {
"label": "Varsayılan Panel Olarak Ata"
"label": "Öntanımlı panel olarak ata"
},
"settings": {
"label": "Ayarları aç"
@@ -2676,8 +2681,8 @@
"title": "Genel komutlar",
"option": {
"colorScheme": {
"light": "Aydınlık moda geç",
"dark": "Koyu moda geç"
"light": "Aydınlık Temaya Geç",
"dark": "Koyu Temaya Geç"
},
"language": {
"label": "Dili değiştir",
@@ -2688,16 +2693,16 @@
}
},
"newBoard": {
"label": "Yeni bir panel oluştur"
"label": "Yeni Panel Oluştur"
},
"importBoard": {
"label": "Bir paneli içe aktar"
},
"newApp": {
"label": "Yeni bir uygulama oluştur"
"label": "Yeni Uygulama Oluştur"
},
"newIntegration": {
"label": "Yeni bir entegrasyon oluşturun",
"label": "Yeni Entegrasyon Oluştur",
"children": {
"detail": {
"title": "Oluşturmak istediğiniz entegrasyon türünü seçin"
@@ -2705,13 +2710,13 @@
}
},
"newUser": {
"label": "Yeni bir kullanıcı oluştur"
"label": "Yeni Kullanıcı Oluştur"
},
"newInvite": {
"label": "Yeni bir davet oluştur"
"label": "Yeni Davet Oluştur"
},
"newGroup": {
"label": "Yeni bir grup oluştur"
"label": "Yeni Grup Oluştur"
}
}
}
@@ -2721,7 +2726,7 @@
"help": "Harici bir arama motoru kullanın",
"group": {
"searchEngine": {
"title": "Arama motorları",
"title": "Arama Motorları",
"children": {
"action": {
"search": {
@@ -2729,7 +2734,7 @@
}
},
"detail": {
"title": "Arama motoru için bir eylem seçin"
"title": "Arama yapmak istediğiniz terimleri girin"
},
"searchResults": {
"title": "Eylemler için bir arama sonucu seçin"
@@ -2840,7 +2845,7 @@
"label": "Hakkında"
},
"homeBoard": {
"label": "Varsayılan Panel"
"label": "Öntanımlı panel"
},
"preferences": {
"label": "Tercihleriniz"
@@ -2894,7 +2899,7 @@
"label": "İsim"
},
"short": {
"label": "Kısa Ad"
"label": "Kısa Yol"
},
"urlTemplate": {
"label": "URL arama şablonu"
@@ -2907,13 +2912,13 @@
"list": {
"title": "Arama Motorları",
"noResults": {
"title": "Henüz bir arama motoru yok",
"title": "Henüz bir arama motoru yapılandırılmadı",
"action": "İlk arama motorunuzu oluşturun"
},
"interactive": "Etkileşimli, entegrasyon kullanır"
},
"create": {
"title": "Yeni arama motoru",
"title": "Yeni Arama Motoru",
"notification": {
"success": {
"title": "Arama motoru oluşturuldu",
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "使用者",
"name": "名稱",
"id": ""
}
},
"downloads": {
"name": "",
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+6 -1
View File
@@ -1457,7 +1457,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {}
"option": {},
"items": {
"user": "",
"name": "",
"id": ""
}
},
"downloads": {
"name": "",
+1 -1
View File
@@ -34,7 +34,7 @@
"@mantine/hooks": "^7.15.2",
"@tabler/icons-react": "^3.26.0",
"mantine-react-table": "2.0.0-beta.7",
"next": "^14.2.21",
"next": "^14.2.22",
"react": "^19.0.0"
},
"devDependencies": {
+13
View File
@@ -15,8 +15,21 @@ const searchSchema = z.object({
limit: z.number().int().positive().default(10),
});
const mediaRequestOptionsSchema = z.object({
mediaId: z.number(),
mediaType: z.enum(["tv", "movie"]),
});
const requestMediaSchema = z.object({
mediaType: z.enum(["tv", "movie"]),
mediaId: z.number(),
seasons: z.array(z.number().min(0)).optional(),
});
export const commonSchemas = {
paginated: paginatedSchema,
byId: byIdSchema,
search: searchSchema,
mediaRequestOptions: mediaRequestOptionsSchema,
requestMedia: requestMediaSchema,
};
+1
View File
@@ -15,6 +15,7 @@ const integrationCreateSchema = z.object({
value: z.string().nonempty(),
}),
),
attemptSearchEngineCreation: z.boolean(),
});
const integrationUpdateSchema = z.object({
Loaded 100 of 113 files, more files were not shown because too many files have changed in this diff. Show more