Compare commits
18
Commits
No files matched your search
@@ -31,6 +31,12 @@
|
||||
automerge: false,
|
||||
},
|
||||
],
|
||||
// Updates lockfile for prebuilt-debian after if package.json is updated, otherwise only root lockfile is updated
|
||||
postUpgradeTasks: {
|
||||
commands: ["cd deployments/prebuilt-debian && pnpm install --ignore-workspace --lockfile-only"],
|
||||
fileFilters: ["deployments/prebuilt-debian/pnpm-lock.yaml"],
|
||||
executionMode: "update",
|
||||
},
|
||||
updateInternalDeps: true,
|
||||
rangeStrategy: "bump",
|
||||
automerge: false,
|
||||
|
||||
@@ -1,3 +1,41 @@
|
||||
## [1.58.0](https://github.com/homarr-labs/homarr/compare/v1.57.1...v1.58.0) (2026-04-03)
|
||||
|
||||
### Features
|
||||
|
||||
* **integrations:** add search.ch integration with timetable widget ([#5180](https://github.com/homarr-labs/homarr/issues/5180)) ([abb2dfc](https://github.com/homarr-labs/homarr/commit/abb2dfc92ab51b16e7172374cfee8b9c0c752454))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **boards:** unable to click on links at settings page ([#5230](https://github.com/homarr-labs/homarr/issues/5230)) ([6a200c3](https://github.com/homarr-labs/homarr/commit/6a200c39afaa1a9a1057643612065a40ad1cdd82))
|
||||
* **deps:** update dependency @ctrl/deluge to ^7.5.3 ([#5365](https://github.com/homarr-labs/homarr/issues/5365)) ([9c134c5](https://github.com/homarr-labs/homarr/commit/9c134c5ceb742b21b021ef3b49de8cc0d526d460))
|
||||
* **deps:** update dependency @ctrl/deluge to v8 ([#5367](https://github.com/homarr-labs/homarr/issues/5367)) ([e88e3d1](https://github.com/homarr-labs/homarr/commit/e88e3d17c01dab8836bec11d2ac01195f06cdc2e))
|
||||
* **deps:** update dependency @immich/sdk to ^2.6.2 ([#5370](https://github.com/homarr-labs/homarr/issues/5370)) ([4a5bafa](https://github.com/homarr-labs/homarr/commit/4a5bafa79092b209e94a56f72362011f416bf115))
|
||||
* **deps:** update dependency @immich/sdk to ^2.6.3 ([#5380](https://github.com/homarr-labs/homarr/issues/5380)) ([2dd3c90](https://github.com/homarr-labs/homarr/commit/2dd3c906c4215b55e23f4b15ba4b423e23be8376))
|
||||
* **deps:** update dependency @tabler/icons-react to ^3.41.0 ([#5384](https://github.com/homarr-labs/homarr/issues/5384)) ([a9bcc9d](https://github.com/homarr-labs/homarr/commit/a9bcc9d04bdfcf28ea561cd590ca2a4bd775593c))
|
||||
* **deps:** update dependency better-sqlite3 to ^12.8.0 ([#5266](https://github.com/homarr-labs/homarr/issues/5266)) ([46db8b6](https://github.com/homarr-labs/homarr/commit/46db8b6ab32406fb4ff7a7a9938692954778a070))
|
||||
* **deps:** update dependency drizzle-orm to ^0.45.2 ([#5388](https://github.com/homarr-labs/homarr/issues/5388)) ([0a78e3f](https://github.com/homarr-labs/homarr/commit/0a78e3f12aa99509f42d7af647a478c5abaf5c96))
|
||||
* **deps:** update dependency fast-xml-parser to ^5.5.9 ([#5359](https://github.com/homarr-labs/homarr/issues/5359)) ([189806b](https://github.com/homarr-labs/homarr/commit/189806b5a2b8e67f925cc8def6bc8ddefdc5bfc4))
|
||||
* **deps:** update dependency fastify to ^5.8.4 ([#5351](https://github.com/homarr-labs/homarr/issues/5351)) ([728f7e4](https://github.com/homarr-labs/homarr/commit/728f7e489aa499bcd7e97c7ec8571e29e7e2cab3))
|
||||
* **deps:** update dependency isomorphic-dompurify to ^3.7.0 ([#5366](https://github.com/homarr-labs/homarr/issues/5366)) ([c2b54dd](https://github.com/homarr-labs/homarr/commit/c2b54ddfaab93daa4e65c0c38bf5c27e4402a492))
|
||||
* **deps:** update dependency isomorphic-dompurify to ^3.7.1 ([#5369](https://github.com/homarr-labs/homarr/issues/5369)) ([8d1ff40](https://github.com/homarr-labs/homarr/commit/8d1ff4065d669268f0e6a03f2b68c1ba728a756a))
|
||||
* **deps:** update dependency jotai to ^2.19.0 ([#5368](https://github.com/homarr-labs/homarr/issues/5368)) ([14d8717](https://github.com/homarr-labs/homarr/commit/14d87178b5a93bdc6fb74793feed95d014814d0b))
|
||||
* **deps:** update dependency node-ical to ^0.23.1 - abandoned ([#4856](https://github.com/homarr-labs/homarr/issues/4856)) ([b07c545](https://github.com/homarr-labs/homarr/commit/b07c545f17f4dafa62167047baa9eed4b7ee4395))
|
||||
* **deps:** update dependency typescript-eslint to ^8.57.2 ([#5360](https://github.com/homarr-labs/homarr/issues/5360)) ([cefbf0e](https://github.com/homarr-labs/homarr/commit/cefbf0e2715e9adbc72cdb0b83d84c309e3257d6))
|
||||
* **deps:** update dependency undici to v7.24.6 ([#5374](https://github.com/homarr-labs/homarr/issues/5374)) ([4f2cc9a](https://github.com/homarr-labs/homarr/commit/4f2cc9af2a42e19624def3bdea60fcb00334c483))
|
||||
* **deps:** update dependency zod to ^4.3.5 - abandoned ([#4803](https://github.com/homarr-labs/homarr/issues/4803)) ([2b4c83d](https://github.com/homarr-labs/homarr/commit/2b4c83dccac168ffa6f9b6e184ed004659373cf7))
|
||||
* **deps:** update dependency zod to ^4.3.6 ([#5376](https://github.com/homarr-labs/homarr/issues/5376)) ([a9582f0](https://github.com/homarr-labs/homarr/commit/a9582f04edb0e161d1fab6e64b13d0ce463ef341))
|
||||
* **deps:** update tanstack-query monorepo to ^5.95.0 ([#5325](https://github.com/homarr-labs/homarr/issues/5325)) ([b11bf36](https://github.com/homarr-labs/homarr/commit/b11bf367d0567e4c8b9068804442ebd200dab01c))
|
||||
* **deps:** update tanstack-query monorepo to ^5.95.1 ([#5355](https://github.com/homarr-labs/homarr/issues/5355)) ([332b6b4](https://github.com/homarr-labs/homarr/commit/332b6b4f42e7c90570cd4c25a6e6a331a76c660f))
|
||||
* **deps:** update tanstack-query monorepo to ^5.95.2 ([#5357](https://github.com/homarr-labs/homarr/issues/5357)) ([6820991](https://github.com/homarr-labs/homarr/commit/682099189b689ea17e16af41fd280a72ed68016f))
|
||||
* **deps:** update tiptap monorepo to v3.20.5 ([#5371](https://github.com/homarr-labs/homarr/issues/5371)) ([6820f8a](https://github.com/homarr-labs/homarr/commit/6820f8afea622587c1ee18b13d13a041ca0572ee))
|
||||
* **deps:** update tiptap monorepo to v3.21.0 ([#5386](https://github.com/homarr-labs/homarr/issues/5386)) ([b30dbaf](https://github.com/homarr-labs/homarr/commit/b30dbafbbbad468ff37ff11237a64ec29df62345))
|
||||
* **deps:** update trpc monorepo to ^11.15.0 ([#5354](https://github.com/homarr-labs/homarr/issues/5354)) ([2fb2981](https://github.com/homarr-labs/homarr/commit/2fb2981d2fb79ca2ca6a9bc5c08d91c8fa5710e7))
|
||||
* **deps:** update trpc monorepo to ^11.15.1 ([#5381](https://github.com/homarr-labs/homarr/issues/5381)) ([451e68a](https://github.com/homarr-labs/homarr/commit/451e68ab57551e23dd7ae772b7483cc56d57ebbb))
|
||||
* **deps:** update trpc monorepo to ^11.16.0 ([#5392](https://github.com/homarr-labs/homarr/issues/5392)) ([72a93e6](https://github.com/homarr-labs/homarr/commit/72a93e681f3701459176692857220730c279f80b))
|
||||
* **deps:** update turbo monorepo to ^2.8.21 ([#5391](https://github.com/homarr-labs/homarr/issues/5391)) ([fa40b6b](https://github.com/homarr-labs/homarr/commit/fa40b6b647b884a7f10c268a22330e0e68b50d4d))
|
||||
* honor trusted certificates in Immich integration requests ([#5275](https://github.com/homarr-labs/homarr/issues/5275)) ([7679553](https://github.com/homarr-labs/homarr/commit/7679553f16ff2945a3eb6cb668ca3a701d13d5ce)), closes [#5242](https://github.com/homarr-labs/homarr/issues/5242)
|
||||
* node-ical prevents many trpc queries from loading ([#5395](https://github.com/homarr-labs/homarr/issues/5395)) ([c977b84](https://github.com/homarr-labs/homarr/commit/c977b84e2d84d5724219f5ff5f99bc550438e168))
|
||||
|
||||
## [1.57.1](https://github.com/homarr-labs/homarr/compare/v1.57.0...v1.57.1) (2026-03-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -55,14 +55,14 @@
|
||||
"@mantine/modals": "^8.3.18",
|
||||
"@mantine/tiptap": "^8.3.18",
|
||||
"@million/lint": "1.0.14",
|
||||
"@tabler/icons-react": "^3.40.0",
|
||||
"@tabler/icons-react": "^3.41.0",
|
||||
"@tanstack/react-query": "^5.95.2",
|
||||
"@tanstack/react-query-devtools": "^5.95.2",
|
||||
"@tanstack/react-query-next-experimental": "^5.95.2",
|
||||
"@trpc/client": "^11.15.0",
|
||||
"@trpc/next": "^11.15.0",
|
||||
"@trpc/react-query": "^11.15.0",
|
||||
"@trpc/server": "^11.15.0",
|
||||
"@trpc/client": "^11.16.0",
|
||||
"@trpc/next": "^11.16.0",
|
||||
"@trpc/react-query": "^11.16.0",
|
||||
"@trpc/server": "^11.16.0",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "0.11.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
|
||||
+11
-4
@@ -302,6 +302,13 @@
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr><td align="center">
|
||||
<a href="https://homarr.dev/docs/integrations/search-ch" target="_blank" rel="noreferrer noopener">
|
||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/search-ch.png" alt="Search.ch" width="90" height="90" />
|
||||
<br/>
|
||||
<p align="center">Search.ch</p>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://homarr.dev/docs/integrations/seerr" target="_blank" rel="noreferrer noopener">
|
||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/seerr.svg" alt="Seerr" width="90" height="90" />
|
||||
<br/>
|
||||
@@ -342,15 +349,15 @@
|
||||
<br/>
|
||||
<p align="center">Transmission</p>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
</td></tr>
|
||||
<tr><td align="center">
|
||||
<a href="https://homarr.dev/docs/integrations/truenas" target="_blank" rel="noreferrer noopener">
|
||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/truenas.svg" alt="TrueNAS" width="90" height="90" />
|
||||
<br/>
|
||||
<p align="center">TrueNAS</p>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr><td align="center">
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://homarr.dev/docs/integrations/unifi-controller" target="_blank" rel="noreferrer noopener">
|
||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png" alt="Unifi Controller" width="90" height="90" />
|
||||
<br/>
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homarr",
|
||||
"version": "1.57.1",
|
||||
"version": "1.58.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env CI=true turbo build",
|
||||
@@ -43,18 +43,18 @@
|
||||
"@semantic-release/npm": "^13.1.5",
|
||||
"@semantic-release/release-notes-generator": "^14.1.0",
|
||||
"@testcontainers/redis": "^11.13.0",
|
||||
"@turbo/gen": "^2.8.20",
|
||||
"@turbo/gen": "^2.8.21",
|
||||
"@vitejs/plugin-react": "^5.2.0",
|
||||
"@vitest/coverage-v8": "^4.1.2",
|
||||
"@vitest/ui": "^4.1.2",
|
||||
"conventional-changelog-conventionalcommits": "^9.3.0",
|
||||
"conventional-changelog-conventionalcommits": "^9.3.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"jsdom": "^29.0.1",
|
||||
"json5": "^2.2.3",
|
||||
"prettier": "^3.8.1",
|
||||
"semantic-release": "^25.0.3",
|
||||
"testcontainers": "^11.13.0",
|
||||
"turbo": "^2.8.20",
|
||||
"turbo": "^2.8.21",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-tsconfig-paths": "^6.1.1",
|
||||
"vitest": "^4.1.2"
|
||||
@@ -79,7 +79,7 @@
|
||||
"overrides": {
|
||||
"@babel/helpers@<7.26.10": ">=7.29.2",
|
||||
"@babel/runtime@<7.26.10": ">=7.29.2",
|
||||
"axios@>=1.0.0 <1.8.2": ">=1.13.6",
|
||||
"axios@>=1.0.0 <1.8.2": ">=1.14.0",
|
||||
"brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.5",
|
||||
"brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.5",
|
||||
"esbuild@<=0.24.2": ">=0.27.4",
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@kubernetes/client-node": "^1.4.0",
|
||||
"@tanstack/react-query": "^5.95.2",
|
||||
"@trpc/client": "^11.15.0",
|
||||
"@trpc/react-query": "^11.15.0",
|
||||
"@trpc/server": "^11.15.0",
|
||||
"@trpc/tanstack-react-query": "^11.15.0",
|
||||
"@trpc/client": "^11.16.0",
|
||||
"@trpc/react-query": "^11.16.0",
|
||||
"@trpc/server": "^11.16.0",
|
||||
"@trpc/tanstack-react-query": "^11.16.0",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"next": "16.2.1",
|
||||
"react": "19.2.4",
|
||||
|
||||
@@ -26,5 +26,6 @@ export const widgetRouter = createTRPCRouter({
|
||||
networkController: lazy(() => import("./network-controller").then((mod) => mod.networkControllerRouter)),
|
||||
firewall: lazy(() => import("./firewall").then((mod) => mod.firewallRouter)),
|
||||
notifications: lazy(() => import("./notifications").then((mod) => mod.notificationsRouter)),
|
||||
timetable: lazy(() => import("./timetable").then((mod) => mod.timetableRouter)),
|
||||
tracearr: lazy(() => import("./tracearr").then((mod) => mod.tracearrRouter)),
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import z from "zod";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { timetableOptionsSchema } from "@homarr/integrations/types";
|
||||
import {
|
||||
timetableGetTimetableRequestHandler,
|
||||
timetableSearchStationsRequestHandler,
|
||||
} from "@homarr/request-handler/timetable";
|
||||
|
||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const timetableRouter = createTRPCRouter({
|
||||
getTimetable: publicProcedure
|
||||
.concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("timetable")))
|
||||
.input(timetableOptionsSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const innerHandler = timetableGetTimetableRequestHandler.handler(ctx.integration, {
|
||||
stationId: input.stationId,
|
||||
limit: input.limit,
|
||||
});
|
||||
|
||||
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
|
||||
return data;
|
||||
}),
|
||||
searchStations: publicProcedure
|
||||
.concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("timetable")))
|
||||
.input(
|
||||
z.object({
|
||||
query: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const innerHandler = timetableSearchStationsRequestHandler.handler(ctx.integration, {
|
||||
query: input.query,
|
||||
});
|
||||
|
||||
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
|
||||
return data;
|
||||
}),
|
||||
});
|
||||
@@ -40,7 +40,7 @@
|
||||
"@t3-oss/env-nextjs": "^0.13.11",
|
||||
"better-sqlite3": "^12.8.0",
|
||||
"dns-caching": "^0.2.9",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"ioredis": "5.10.1",
|
||||
"mysql2": "3.20.0",
|
||||
"pg": "^8.20.0",
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
"@homarr/core": "workspace:^0.1.0",
|
||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||
"@tanstack/react-query": "^5.95.2",
|
||||
"@trpc/client": "^11.15.0",
|
||||
"@trpc/server": "^11.15.0",
|
||||
"@trpc/tanstack-react-query": "^11.15.0",
|
||||
"@trpc/client": "^11.16.0",
|
||||
"@trpc/server": "^11.16.0",
|
||||
"@trpc/tanstack-react-query": "^11.16.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"react": "19.2.4",
|
||||
"zod": "^4.3.6"
|
||||
|
||||
@@ -18,6 +18,7 @@ import { mediaServerJob } from "./jobs/integrations/media-server";
|
||||
import { mediaTranscodingJob } from "./jobs/integrations/media-transcoding";
|
||||
import { networkControllerJob } from "./jobs/integrations/network-controller";
|
||||
import { refreshNotificationsJob } from "./jobs/integrations/notifications";
|
||||
import { timetableJob } from "./jobs/integrations/timetable";
|
||||
import { tracearrJob } from "./jobs/integrations/tracearr";
|
||||
import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
|
||||
import { pingJob } from "./jobs/ping";
|
||||
@@ -51,6 +52,7 @@ export const jobGroup = createCronJobGroup({
|
||||
firewallInterfaces: firewallInterfacesJob,
|
||||
refreshNotifications: refreshNotificationsJob,
|
||||
weather: weatherJob,
|
||||
timetable: timetableJob,
|
||||
tracearr: tracearrJob,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions";
|
||||
import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
|
||||
import { timetableGetTimetableRequestHandler } from "@homarr/request-handler/timetable";
|
||||
|
||||
import { createCronJob } from "../../lib";
|
||||
|
||||
export const timetableJob = createCronJob("timetable", EVERY_5_MINUTES).withCallback(
|
||||
createRequestIntegrationJobHandler(timetableGetTimetableRequestHandler.handler, {
|
||||
widgetKinds: ["timetable"],
|
||||
getInput: {
|
||||
timetable: (options) =>
|
||||
options.station
|
||||
? {
|
||||
stationId: options.station.value,
|
||||
limit: 10,
|
||||
}
|
||||
: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -54,7 +54,7 @@
|
||||
"better-sqlite3": "^12.8.0",
|
||||
"dotenv": "^17.3.1",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"drizzle-zod": "^0.8.3",
|
||||
"mysql2": "3.20.0",
|
||||
"pg": "^8.20.0",
|
||||
|
||||
@@ -334,6 +334,15 @@ export const integrationDefs = {
|
||||
// @ts-expect-error - docs page will be created when integration is merged
|
||||
documentationUrl: createDocumentationLink("/docs/integrations/coolify"),
|
||||
},
|
||||
searchCh: {
|
||||
name: "Search.ch",
|
||||
secretKinds: [[]],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/search-ch.png",
|
||||
defaultUrl: "https://search.ch",
|
||||
category: ["timetable"],
|
||||
// @ts-expect-error - docs page will be created when integration is merged
|
||||
documentationUrl: createDocumentationLink("/docs/integrations/search-ch"),
|
||||
},
|
||||
immich: {
|
||||
name: "Immich",
|
||||
secretKinds: [["apiKey"]],
|
||||
@@ -434,6 +443,7 @@ export const integrationCategories = [
|
||||
"releasesProvider",
|
||||
"notifications",
|
||||
"firewall",
|
||||
"timetable",
|
||||
"photoService",
|
||||
"mediaMonitoring",
|
||||
] as const;
|
||||
|
||||
@@ -31,6 +31,7 @@ export const widgetKinds = [
|
||||
"systemResources",
|
||||
"coolify",
|
||||
"systemDisks",
|
||||
"timetable",
|
||||
"immich-serverStats",
|
||||
"immich-albumCarousel",
|
||||
"tracearr",
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
"@octokit/auth-app": "^8.2.0",
|
||||
"ical.js": "^2.2.1",
|
||||
"maria2": "^0.4.1",
|
||||
"node-ical": "^0.23.1",
|
||||
"octokit": "^5.0.5",
|
||||
"proxmox-api": "1.1.1",
|
||||
"timezones-ical-library": "^2.1.3",
|
||||
"tsdav": "^2.1.8",
|
||||
"undici": "7.24.6",
|
||||
"xml2js": "^0.6.2",
|
||||
|
||||
@@ -40,6 +40,7 @@ import { PlexIntegration } from "../plex/plex-integration";
|
||||
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
|
||||
import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
|
||||
import { QuayIntegration } from "../quay/quay-integration";
|
||||
import { SearchChIntegration } from "../search-ch/search-ch-integration";
|
||||
import { SeerrIntegration } from "../seerr/seerr-integration";
|
||||
import { TracearrIntegration } from "../tracearr/tracearr-integration";
|
||||
import { TrueNasIntegration } from "../truenas/truenas-integration";
|
||||
@@ -114,6 +115,7 @@ export const integrationCreators = {
|
||||
coolify: CoolifyIntegration,
|
||||
tracearr: TracearrIntegration,
|
||||
glances: GlancesIntegration,
|
||||
searchCh: SearchChIntegration,
|
||||
immich: ImmichIntegration,
|
||||
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Station, Timetable, TimetableOptions } from "./timetable-types";
|
||||
|
||||
export interface TimetableIntegration {
|
||||
searchStationsAsync(query: string): Promise<Station[]>;
|
||||
getTimetableAsync(options: TimetableOptions): Promise<Timetable>;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import z from "zod";
|
||||
|
||||
export interface Station {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const timetableOptionsSchema = z.object({
|
||||
stationId: z.string(),
|
||||
limit: z.number().default(10),
|
||||
});
|
||||
|
||||
export type TimetableOptions = z.infer<typeof timetableOptionsSchema>;
|
||||
|
||||
export interface TimetableEntry {
|
||||
timestamp: Date;
|
||||
delay: number;
|
||||
line: {
|
||||
name: string;
|
||||
color: string | null;
|
||||
} | null;
|
||||
location: string;
|
||||
platform: {
|
||||
name: string;
|
||||
hasChanged: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface Timetable {
|
||||
stationId: string;
|
||||
timestamp: Date;
|
||||
entries: TimetableEntry[];
|
||||
}
|
||||
@@ -2,8 +2,9 @@ import type { Agent } from "https";
|
||||
import dayjs from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import ICAL from "ical.js";
|
||||
import type { RequestInit as NodeFetchRequestInit } from "node-fetch";
|
||||
import * as ical from "node-ical";
|
||||
import { tzlib_get_ical_block, tzlib_get_timezones } from "timezones-ical-library";
|
||||
import { DAVClient } from "tsdav";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -21,6 +22,20 @@ import type { CalendarEvent } from "../interfaces/calendar/calendar-types";
|
||||
import type { Notification } from "../interfaces/notifications/notification-types";
|
||||
import type { INotificationsIntegration } from "../interfaces/notifications/notifications-integration";
|
||||
|
||||
// Register all existing timezones
|
||||
if (ICAL.TimezoneService.count === 0) {
|
||||
const timezones = tzlib_get_timezones() as string[];
|
||||
for (const tzid of timezones) {
|
||||
ICAL.TimezoneService.register(
|
||||
new ICAL.Timezone({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-non-null-assertion
|
||||
component: new ICAL.Component(ICAL.parse((tzlib_get_ical_block(tzid) as string[])[0]!)),
|
||||
tzid,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const notificationSchema = z.object({
|
||||
ocs: z.object({
|
||||
data: z.array(
|
||||
@@ -68,39 +83,64 @@ export class NextcloudIntegration extends Integration implements ICalendarIntegr
|
||||
|
||||
return calendarEvents
|
||||
.map((event) => {
|
||||
// @ts-expect-error the typescript definitions for this package are wrong
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
|
||||
const icalData = ical.default.parseICS(event.data) as ical.CalendarResponse;
|
||||
const veventObject = Object.values(icalData).find((data) => data.type === "VEVENT");
|
||||
|
||||
if (!veventObject) {
|
||||
throw new Error(`Invalid event data object: ${JSON.stringify(event.data)}. Unable to process the calendar.`);
|
||||
if (typeof event.data !== "string") {
|
||||
return [];
|
||||
}
|
||||
const comp = ICAL.Component.fromString(event.data);
|
||||
const veventComponent = comp.getFirstSubcomponent("vevent");
|
||||
|
||||
if (!veventComponent) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const veventObject = new ICAL.Event(veventComponent);
|
||||
|
||||
logger.debug(`Converting VEVENT event to ${event.etag} from Nextcloud: ${JSON.stringify(veventObject)}`);
|
||||
|
||||
const eventUrlWithoutHost = new URL(event.url).pathname;
|
||||
const eventSlug = Buffer.from(eventUrlWithoutHost).toString("base64url");
|
||||
|
||||
const startDates = veventObject.rrule ? veventObject.rrule.between(start, end) : [veventObject.start];
|
||||
const durationMs = veventObject.duration.toSeconds() * 1000;
|
||||
|
||||
const durationMs = veventObject.end.getTime() - veventObject.start.getTime();
|
||||
// Handle recurring events
|
||||
let startDates: Date[];
|
||||
if (veventObject.isRecurring()) {
|
||||
const iterator = veventObject.iterator();
|
||||
startDates = [];
|
||||
|
||||
// Iterate through occurrences within the time range
|
||||
let next: ICAL.Time | undefined;
|
||||
// next actually returns undefined when there are no more occurrences
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
while ((next = iterator.next())) {
|
||||
const nextStartDate = next.convertToZone(ICAL.Timezone.utcTimezone).toJSDate();
|
||||
if (nextStartDate > end) break;
|
||||
const nextEndDate = new Date(nextStartDate.getTime() + durationMs);
|
||||
if (nextEndDate < start) continue;
|
||||
|
||||
startDates.push(nextStartDate);
|
||||
}
|
||||
} else {
|
||||
startDates = [veventObject.startDate.convertToZone(ICAL.Timezone.utcTimezone).toJSDate()];
|
||||
}
|
||||
|
||||
return startDates.map((startDate) => {
|
||||
const utcStartDate = dayjs(startDate).tz("UTC").toDate();
|
||||
const endDate = new Date(utcStartDate.getTime() + durationMs);
|
||||
const dateInMillis = utcStartDate.valueOf();
|
||||
const endDate = new Date(startDate.getTime() + durationMs);
|
||||
const dateInMillis = startDate.valueOf();
|
||||
|
||||
// Get color property from the component
|
||||
const colorProperty = veventComponent.getFirstProperty("color");
|
||||
const color = colorProperty?.getFirstValue() as string | undefined;
|
||||
|
||||
return {
|
||||
title: veventObject.summary,
|
||||
subTitle: null,
|
||||
description: veventObject.description,
|
||||
startDate: utcStartDate,
|
||||
description: veventObject.description || null,
|
||||
startDate,
|
||||
endDate,
|
||||
image: null,
|
||||
location: veventObject.location || null,
|
||||
indicatorColor:
|
||||
"color" in veventObject && typeof veventObject.color === "string" ? veventObject.color : "#ff8600",
|
||||
indicatorColor: color ?? "#ff8600",
|
||||
links: [
|
||||
{
|
||||
href: this.externalUrl(
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import z from "zod";
|
||||
|
||||
import { ResponseError } from "@homarr/common/server";
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||
|
||||
import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { TimetableIntegration } from "../interfaces/timetable/timetable-integration";
|
||||
import type { Station, Timetable, TimetableOptions } from "../interfaces/timetable/timetable-types";
|
||||
|
||||
const supportedStationTypes = ["bus", "tram", "train", "ship", "cablecar", "funicular", "chairlift"];
|
||||
|
||||
export class SearchChIntegration extends Integration implements TimetableIntegration {
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const response = await input.fetchAsync(this.url("/timetable/api/completion.json", { term: "Zürich" }));
|
||||
|
||||
if (!response.ok) {
|
||||
return TestConnectionError.StatusResult(response);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for stations matching the given query.
|
||||
* @param query The search query for station names.
|
||||
* @returns A list of stations matching the search query.
|
||||
* @docs https://search.ch/fahrplan/api/help
|
||||
*/
|
||||
public async searchStationsAsync(query: string): Promise<Station[]> {
|
||||
const response = await fetchWithTrustedCertificatesAsync(
|
||||
this.url("/timetable/api/completion.json", {
|
||||
term: query,
|
||||
show_ids: 1,
|
||||
nofavorites: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ResponseError(response);
|
||||
}
|
||||
|
||||
const data = await searchSchema.parseAsync(await response.json());
|
||||
|
||||
return (
|
||||
data
|
||||
// Remove completions like addresses, we only want stations.
|
||||
.filter((item) => supportedStationTypes.some((type) => item.iconclass.endsWith(type)))
|
||||
.map((item) => (item.id !== undefined ? { id: item.id, name: item.label } : null))
|
||||
.filter((item) => item !== null)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the departure timetable for a given station.
|
||||
* @param options Timetable options including station ID and limit.
|
||||
* @returns timetable for the given station.
|
||||
* @docs https://search.ch/fahrplan/api/help
|
||||
*/
|
||||
public async getTimetableAsync(options: TimetableOptions): Promise<Timetable> {
|
||||
const now = new Date();
|
||||
// The following format is used [01.01.2000, 23:59:00]. The API allows usage of DD.MM.YYYY format and HH:mm format
|
||||
const [date, time] = now.toLocaleString("en-CH", { timeZone: "Europe/Zurich" }).split(", ") as [string, string];
|
||||
|
||||
const response = await fetchWithTrustedCertificatesAsync(
|
||||
this.url("/timetable/api/stationboard.json", {
|
||||
stop: options.stationId,
|
||||
limit: options.limit,
|
||||
show_delays: 1,
|
||||
show_tracks: 1,
|
||||
date,
|
||||
time: time.substring(0, 5), // Only keep the HH:mm part of the time, as that's what the API expects
|
||||
}),
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ResponseError(response);
|
||||
}
|
||||
|
||||
const data = await timetableSchema.parseAsync(await response.json());
|
||||
|
||||
return {
|
||||
stationId: options.stationId,
|
||||
timestamp: now,
|
||||
entries: data.connections.map((connection) => {
|
||||
// Color is in format A35~F10~ for foreground and background color. Some only have ~~ and therefore no color
|
||||
const color = connection.color.split("~")[0];
|
||||
|
||||
return {
|
||||
timestamp: connection.time,
|
||||
line: connection.line
|
||||
? {
|
||||
name: connection.line,
|
||||
color: color && color.length >= 1 ? `#${color}` : null,
|
||||
}
|
||||
: null,
|
||||
location: connection.terminal.name,
|
||||
delay: connection.dep_delay,
|
||||
platform: connection.track
|
||||
? {
|
||||
// If the track name contains "!", it indicates a change in the track
|
||||
// so we remove the "!" from the name and set hasChanged to true
|
||||
name: connection.track.replace("!", ""),
|
||||
hasChanged: connection.track.includes("!"),
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const timetableSchema = z.object({
|
||||
connections: z.array(
|
||||
z.object({
|
||||
time: z.string().transform((str) => new Date(str)),
|
||||
line: z.string().optional(),
|
||||
color: z.string(),
|
||||
terminal: z.object({
|
||||
name: z.string(),
|
||||
}),
|
||||
track: z.string().optional(),
|
||||
dep_delay: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((str) => (str ? parseInt(str) : 0)),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const searchSchema = z.array(
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
label: z.string(),
|
||||
iconclass: z.string(), // This is in format sl-icon-type-{type}
|
||||
}),
|
||||
);
|
||||
@@ -12,4 +12,5 @@ export * from "./unifi-controller/unifi-controller-types";
|
||||
export * from "./opnsense/opnsense-types";
|
||||
export * from "./interfaces/media-releases";
|
||||
export * from "./coolify/coolify-types";
|
||||
export * from "./interfaces/timetable/timetable-types";
|
||||
export * from "./tracearr/tracearr-types";
|
||||
@@ -0,0 +1,454 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import type { StartedTestContainer } from "testcontainers";
|
||||
import { GenericContainer, getContainerRuntimeClient, ImageName, Wait } from "testcontainers";
|
||||
import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { createId } from "@homarr/common";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
import { NextcloudIntegration } from "../src";
|
||||
|
||||
// Mock the database
|
||||
vi.mock("@homarr/db", async (importActual) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
const actual = await importActual<typeof import("@homarr/db")>();
|
||||
return {
|
||||
...actual,
|
||||
db: createDb(),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock certificate verification
|
||||
vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
const actual = await importActual<typeof import("@homarr/core/infrastructure/certificates")>();
|
||||
return {
|
||||
...actual,
|
||||
getTrustedCertificateHostnamesAsync: vi.fn().mockImplementation(() => {
|
||||
return Promise.resolve([]);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const IMAGE_NAME = "nextcloud:latest";
|
||||
const DEFAULT_USERNAME = "admin";
|
||||
const DEFAULT_PASSWORD = "admin123";
|
||||
|
||||
interface CalendarTestEvent {
|
||||
summary: string;
|
||||
description?: string;
|
||||
location?: string;
|
||||
color?: string;
|
||||
dtstart: string;
|
||||
dtend: string;
|
||||
rrule?: string;
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
describe("Nextcloud integration", () => {
|
||||
let startedContainer: StartedTestContainer | null = null;
|
||||
let nextcloudIntegration: NextcloudIntegration | null = null;
|
||||
|
||||
beforeAll(async () => {
|
||||
const containerRuntimeClient = await getContainerRuntimeClient();
|
||||
await containerRuntimeClient.image.pull(ImageName.fromString(IMAGE_NAME));
|
||||
|
||||
startedContainer = await prepareNextcloudContainerAsync();
|
||||
nextcloudIntegration = createNextcloudIntegration(startedContainer);
|
||||
}, 100_000);
|
||||
|
||||
afterAll(async () => {
|
||||
if (startedContainer) {
|
||||
await startedContainer.stop();
|
||||
startedContainer = null;
|
||||
}
|
||||
});
|
||||
|
||||
describe("Test connection", () => {
|
||||
test("Test connection > Should work with valid credentials", async () => {
|
||||
// Act
|
||||
const result = await nextcloudIntegration!.testConnectionAsync();
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(true);
|
||||
}, 20_000);
|
||||
|
||||
test("Test connection > Should fail with invalid credentials", async () => {
|
||||
// Arrange
|
||||
const invalidIntegration = createNextcloudIntegration(startedContainer!, "wrong-user", "wrong-pass");
|
||||
|
||||
// Act
|
||||
const result = await invalidIntegration.testConnectionAsync();
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(false);
|
||||
}, 20_000);
|
||||
});
|
||||
|
||||
describe("Retrieve calendar events", () => {
|
||||
test("Retrieve calendar events > Should retrieve all fields correctly", async () => {
|
||||
// Arrange
|
||||
const calendarName = createId();
|
||||
const testEvent: CalendarTestEvent = {
|
||||
summary: "Test Meeting",
|
||||
description: "This is a test meeting",
|
||||
location: "Conference Room A",
|
||||
color: "#FF5733",
|
||||
dtstart: "20260415T140000Z",
|
||||
dtend: "20260415T150000Z",
|
||||
};
|
||||
const start = new Date("2026-04-01T00:00:00Z");
|
||||
const end = new Date("2026-04-30T23:59:59Z");
|
||||
|
||||
await createCalendarAsync(startedContainer!, calendarName);
|
||||
await createCalendarEventAsync(startedContainer!, calendarName, testEvent);
|
||||
|
||||
// Act
|
||||
const events = await nextcloudIntegration!.getCalendarEventsAsync(start, end);
|
||||
|
||||
// Assert
|
||||
const relevantEvents = events.filter((event) => event.title === testEvent.summary);
|
||||
expect(relevantEvents).toHaveLength(1);
|
||||
const retrievedEvent = relevantEvents[0];
|
||||
expect(retrievedEvent).toMatchObject({
|
||||
title: testEvent.summary,
|
||||
description: testEvent.description,
|
||||
location: testEvent.location,
|
||||
indicatorColor: testEvent.color,
|
||||
});
|
||||
expect(retrievedEvent!.startDate.toISOString()).toBe("2026-04-15T14:00:00.000Z");
|
||||
expect(retrievedEvent!.endDate?.toISOString()).toBe("2026-04-15T15:00:00.000Z");
|
||||
}, 20_000);
|
||||
test("Retrieve calendar events > Should work with only required fields", async () => {
|
||||
// Arrange
|
||||
const calendarName = createId();
|
||||
const testEvent: CalendarTestEvent = {
|
||||
summary: "Simple Event",
|
||||
dtstart: "20260415T140000Z",
|
||||
dtend: "20260415T150000Z",
|
||||
};
|
||||
const start = new Date("2026-04-01T00:00:00Z");
|
||||
const end = new Date("2026-04-30T23:59:59Z");
|
||||
|
||||
await createCalendarAsync(startedContainer!, calendarName);
|
||||
await createCalendarEventAsync(startedContainer!, calendarName, testEvent);
|
||||
|
||||
// Act
|
||||
const events = await nextcloudIntegration!.getCalendarEventsAsync(start, end);
|
||||
|
||||
// Assert
|
||||
const relevantEvents = events.filter((event) => event.title === testEvent.summary);
|
||||
expect(relevantEvents).toHaveLength(1);
|
||||
const retrievedEvent = relevantEvents[0];
|
||||
expect(retrievedEvent?.location).toBeNull();
|
||||
expect(retrievedEvent?.description).toBeNull();
|
||||
expect(retrievedEvent?.indicatorColor).toBe("#ff8600"); // Default color
|
||||
}, 20_000);
|
||||
test("Retrieve calendar events > Should work with timezone specified", async () => {
|
||||
// Arrange
|
||||
const calendarName = createId();
|
||||
const testEvent: CalendarTestEvent = {
|
||||
summary: "Timezone Event",
|
||||
dtstart: "20260415T140000",
|
||||
dtend: "20260415T150000",
|
||||
timezone: "Asia/Tokyo",
|
||||
};
|
||||
const start = new Date("2026-04-01T00:00:00Z");
|
||||
const end = new Date("2026-04-30T23:59:59Z");
|
||||
|
||||
await createCalendarAsync(startedContainer!, calendarName);
|
||||
await createCalendarEventAsync(startedContainer!, calendarName, testEvent);
|
||||
|
||||
// Act
|
||||
const events = await nextcloudIntegration!.getCalendarEventsAsync(start, end);
|
||||
|
||||
// Assert
|
||||
const relevantEvents = events.filter((event) => event.title === testEvent.summary);
|
||||
expect(relevantEvents).toHaveLength(1);
|
||||
const retrievedEvent = relevantEvents[0];
|
||||
|
||||
expect(retrievedEvent!.startDate.toISOString()).toBe("2026-04-15T05:00:00.000Z"); // 14:00 in Tokyo is 05:00 UTC
|
||||
expect(retrievedEvent!.endDate?.toISOString()).toBe("2026-04-15T06:00:00.000Z"); // 15:00 in Tokyo is 06:00 UTC
|
||||
}, 120_000);
|
||||
test("Retrieve calendar events > Should work with event that starts before the range but ends within the range", async () => {
|
||||
// Arrange
|
||||
const calendarName = createId();
|
||||
const testEvent: CalendarTestEvent = {
|
||||
summary: "Early Meeting",
|
||||
dtstart: "20260401T230000Z", // Starts before the range
|
||||
dtend: "20260402T010000Z", // Ends within the range
|
||||
};
|
||||
const start = new Date("2026-04-02T00:00:00Z");
|
||||
const end = new Date("2026-04-30T23:59:59Z");
|
||||
|
||||
await createCalendarAsync(startedContainer!, calendarName);
|
||||
await createCalendarEventAsync(startedContainer!, calendarName, testEvent);
|
||||
|
||||
// Act
|
||||
const events = await nextcloudIntegration!.getCalendarEventsAsync(start, end);
|
||||
|
||||
// Assert
|
||||
const relevantEvents = events.filter((event) => event.title === testEvent.summary);
|
||||
expect(relevantEvents).toHaveLength(1);
|
||||
}, 20_000);
|
||||
test("Retrieve calendar events > Should work with event that starts within the range but ends after the range", async () => {
|
||||
// Arrange
|
||||
const calendarName = createId();
|
||||
const testEvent: CalendarTestEvent = {
|
||||
summary: "Late Meeting",
|
||||
dtstart: "20260430T230000Z", // Starts within the range
|
||||
dtend: "20260501T010000Z", // Ends after the range
|
||||
};
|
||||
const start = new Date("2026-04-01T00:00:00Z");
|
||||
const end = new Date("2026-04-30T23:59:59Z");
|
||||
|
||||
await createCalendarAsync(startedContainer!, calendarName);
|
||||
await createCalendarEventAsync(startedContainer!, calendarName, testEvent);
|
||||
|
||||
// Act
|
||||
const events = await nextcloudIntegration!.getCalendarEventsAsync(start, end);
|
||||
|
||||
// Assert
|
||||
const relevantEvents = events.filter((event) => event.title === testEvent.summary);
|
||||
expect(relevantEvents).toHaveLength(1);
|
||||
}, 20_000);
|
||||
test("Retrieve calendar events > Should not retrieve events outside the range", async () => {
|
||||
// Arrange
|
||||
const calendarName = createId();
|
||||
const testEvent: CalendarTestEvent = {
|
||||
summary: "Out of Range Meeting",
|
||||
dtstart: "20260301T140000Z", // Starts before the range
|
||||
dtend: "20260301T150000Z", // Ends before the range
|
||||
};
|
||||
const start = new Date("2026-04-01T00:00:00Z");
|
||||
const end = new Date("2026-04-30T23:59:59Z");
|
||||
|
||||
await createCalendarAsync(startedContainer!, calendarName);
|
||||
await createCalendarEventAsync(startedContainer!, calendarName, testEvent);
|
||||
|
||||
// Act
|
||||
const events = await nextcloudIntegration!.getCalendarEventsAsync(start, end);
|
||||
|
||||
// Assert
|
||||
const relevantEvents = events.filter((event) => event.title === testEvent.summary);
|
||||
expect(relevantEvents).toHaveLength(0);
|
||||
}, 20_000);
|
||||
test("Retrieve calendar events > Should work with events that begin before the range and end after the range", async () => {
|
||||
// Arrange
|
||||
const calendarName = createId();
|
||||
const testEvent: CalendarTestEvent = {
|
||||
summary: "Spanning Meeting",
|
||||
dtstart: "20260330T230000Z", // Starts before the range
|
||||
dtend: "20260501T010000Z", // Ends after the range
|
||||
};
|
||||
const start = new Date("2026-04-01T00:00:00Z");
|
||||
const end = new Date("2026-04-30T23:59:59Z");
|
||||
|
||||
await createCalendarAsync(startedContainer!, calendarName);
|
||||
await createCalendarEventAsync(startedContainer!, calendarName, testEvent);
|
||||
|
||||
// Act
|
||||
const events = await nextcloudIntegration!.getCalendarEventsAsync(start, end);
|
||||
|
||||
// Assert
|
||||
const relevantEvents = events.filter((event) => event.title === testEvent.summary);
|
||||
expect(relevantEvents).toHaveLength(1);
|
||||
}, 20_000);
|
||||
test("Retrieve calendar events > Should work with recurring events first occurrence before the range", async () => {
|
||||
// Arrange
|
||||
const calendarName = createId();
|
||||
const testEvent: CalendarTestEvent = {
|
||||
summary: "Recurring Meeting",
|
||||
dtstart: "20260330T140000Z", // First occurrence starts before the range
|
||||
dtend: "20260330T150000Z",
|
||||
rrule: "FREQ=DAILY;COUNT=5", // Daily for 5 occurrences
|
||||
};
|
||||
const start = new Date("2026-04-01T00:00:00Z");
|
||||
const end = new Date("2026-04-30T23:59:59Z");
|
||||
|
||||
await createCalendarAsync(startedContainer!, calendarName);
|
||||
await createCalendarEventAsync(startedContainer!, calendarName, testEvent);
|
||||
|
||||
// Act
|
||||
const events = await nextcloudIntegration!.getCalendarEventsAsync(start, end);
|
||||
|
||||
// Assert
|
||||
const relevantEvents = events.filter((event) => event.title === testEvent.summary);
|
||||
expect(relevantEvents).toHaveLength(3);
|
||||
}, 20_000);
|
||||
test("Retrieve calendar events > Should work with recurring events that go beyond the range", async () => {
|
||||
// Arrange
|
||||
const calendarName = createId();
|
||||
const testEvent: CalendarTestEvent = {
|
||||
summary: "Working Week",
|
||||
dtstart: "20260302T090000Z",
|
||||
dtend: "20260306T170000Z",
|
||||
rrule: "FREQ=WEEKLY;COUNT=10",
|
||||
};
|
||||
const start = new Date("2026-04-01T00:00:00Z");
|
||||
const end = new Date("2026-04-30T23:59:59Z");
|
||||
|
||||
await createCalendarAsync(startedContainer!, calendarName);
|
||||
await createCalendarEventAsync(startedContainer!, calendarName, testEvent);
|
||||
|
||||
// Act
|
||||
const events = await nextcloudIntegration!.getCalendarEventsAsync(start, end);
|
||||
|
||||
// Assert
|
||||
const relevantEvents = events.filter((event) => event.title === testEvent.summary);
|
||||
expect(relevantEvents).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Prepares a Nextcloud container with proper configuration
|
||||
*/
|
||||
async function prepareNextcloudContainerAsync(): Promise<StartedTestContainer> {
|
||||
const container = createNextcloudContainer();
|
||||
const startedContainer = await container.start();
|
||||
|
||||
return startedContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Nextcloud container with proper configuration
|
||||
*/
|
||||
function createNextcloudContainer() {
|
||||
return new GenericContainer(IMAGE_NAME)
|
||||
.withEnvironment({
|
||||
NEXTCLOUD_ADMIN_USER: DEFAULT_USERNAME,
|
||||
NEXTCLOUD_ADMIN_PASSWORD: DEFAULT_PASSWORD,
|
||||
NEXTCLOUD_TRUSTED_DOMAINS: "*",
|
||||
SQLITE_DATABASE: "nextcloud", // Use SQLite for faster startup
|
||||
})
|
||||
.withExposedPorts(80)
|
||||
.withWaitStrategy(Wait.forHttp("/status.php", 80).forStatusCode(200))
|
||||
.withStartupTimeout(120_000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Nextcloud integration instance
|
||||
*/
|
||||
function createNextcloudIntegration(
|
||||
container: StartedTestContainer,
|
||||
username?: string,
|
||||
password?: string,
|
||||
): NextcloudIntegration {
|
||||
return new NextcloudIntegration({
|
||||
id: "1",
|
||||
decryptedSecrets: [
|
||||
{
|
||||
kind: "username",
|
||||
value: username ?? DEFAULT_USERNAME,
|
||||
},
|
||||
{
|
||||
kind: "password",
|
||||
value: password ?? DEFAULT_PASSWORD,
|
||||
},
|
||||
],
|
||||
name: "Nextcloud",
|
||||
url: `http://${container.getHost()}:${container.getMappedPort(80)}`,
|
||||
externalUrl: null,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a calendar in Nextcloud via OCS API
|
||||
*/
|
||||
async function createCalendarAsync(container: StartedTestContainer, calendarName: string): Promise<void> {
|
||||
const host = container.getHost();
|
||||
const port = container.getMappedPort(80);
|
||||
const url = `http://${host}:${port}/remote.php/dav/calendars/${DEFAULT_USERNAME}/${calendarName}`;
|
||||
|
||||
const auth = Buffer.from(`${DEFAULT_USERNAME}:${DEFAULT_PASSWORD}`).toString("base64");
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "MKCALENDAR",
|
||||
headers: {
|
||||
Authorization: `Basic ${auth}`,
|
||||
"Content-Type": "application/xml",
|
||||
},
|
||||
body: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<c:mkcalendar xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:">
|
||||
<d:set>
|
||||
<d:prop>
|
||||
<d:displayname>${calendarName}</d:displayname>
|
||||
</d:prop>
|
||||
</d:set>
|
||||
</c:mkcalendar>`,
|
||||
});
|
||||
|
||||
if (!response.ok && response.status !== 201) {
|
||||
throw new Error(`Failed to create calendar: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a calendar event in Nextcloud via CalDAV
|
||||
*/
|
||||
async function createCalendarEventAsync(
|
||||
container: StartedTestContainer,
|
||||
calendarName: string,
|
||||
event: CalendarTestEvent,
|
||||
): Promise<void> {
|
||||
const host = container.getHost();
|
||||
const port = container.getMappedPort(80);
|
||||
const id = createId();
|
||||
const url = `http://${host}:${port}/remote.php/dav/calendars/${DEFAULT_USERNAME}/${calendarName}/${id}.ics`;
|
||||
|
||||
const auth = Buffer.from(`${DEFAULT_USERNAME}:${DEFAULT_PASSWORD}`).toString("base64");
|
||||
|
||||
// Build iCalendar format
|
||||
const icalData = buildICalEvent(id, event);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Basic ${auth}`,
|
||||
"Content-Type": "text/calendar",
|
||||
},
|
||||
body: icalData,
|
||||
});
|
||||
|
||||
if (!response.ok && response.status !== 201) {
|
||||
throw new Error(`Failed to create event: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an iCalendar format string from event data
|
||||
*/
|
||||
function buildICalEvent(uid: string, event: CalendarTestEvent): string {
|
||||
const lines = [
|
||||
"BEGIN:VCALENDAR",
|
||||
"VERSION:2.0",
|
||||
"PRODID:-//Homarr Tests//EN",
|
||||
"BEGIN:VEVENT",
|
||||
`UID:${uid}`,
|
||||
`DTSTAMP:${new Date().toISOString().replace(/[-:]/g, "").split(".")[0]}Z`,
|
||||
`DTSTART${event.timezone ? `;TZID=${event.timezone}` : ""}:${event.dtstart}`,
|
||||
`DTEND${event.timezone ? `;TZID=${event.timezone}` : ""}:${event.dtend}`,
|
||||
`SUMMARY:${event.summary}`,
|
||||
];
|
||||
|
||||
if (event.description) {
|
||||
lines.push(`DESCRIPTION:${event.description}`);
|
||||
}
|
||||
|
||||
if (event.location) {
|
||||
lines.push(`LOCATION:${event.location}`);
|
||||
}
|
||||
|
||||
if (event.color) {
|
||||
lines.push(`COLOR:${event.color}`);
|
||||
}
|
||||
|
||||
if (event.rrule) {
|
||||
lines.push(`RRULE:${event.rrule}`);
|
||||
}
|
||||
|
||||
lines.push("END:VEVENT", "END:VCALENDAR");
|
||||
|
||||
return lines.join("\r\n");
|
||||
}
|
||||
@@ -34,7 +34,7 @@
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^8.3.18",
|
||||
"@tabler/icons-react": "^3.40.0",
|
||||
"@tabler/icons-react": "^3.41.0",
|
||||
"dayjs": "^1.11.20",
|
||||
"next": "16.2.1",
|
||||
"react": "19.2.4",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"dependencies": {
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/notifications": "^8.3.18",
|
||||
"@tabler/icons-react": "^3.40.0"
|
||||
"@tabler/icons-react": "^3.41.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||
import { createIntegrationAsync } from "@homarr/integrations";
|
||||
import type { Station, Timetable } from "@homarr/integrations/types";
|
||||
|
||||
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||
|
||||
export const timetableSearchStationsRequestHandler = createCachedIntegrationRequestHandler<
|
||||
Station[],
|
||||
IntegrationKindByCategory<"timetable">,
|
||||
{ query: string }
|
||||
>({
|
||||
async requestAsync(integration, input) {
|
||||
const integrationInstance = await createIntegrationAsync(integration);
|
||||
return await integrationInstance.searchStationsAsync(input.query);
|
||||
},
|
||||
queryKey: "timetableSearchStations",
|
||||
cacheDuration: dayjs.duration(1, "day"),
|
||||
});
|
||||
|
||||
export const timetableGetTimetableRequestHandler = createCachedIntegrationRequestHandler<
|
||||
Timetable,
|
||||
IntegrationKindByCategory<"timetable">,
|
||||
{ stationId: string; limit: number }
|
||||
>({
|
||||
async requestAsync(integration, input) {
|
||||
const integrationInstance = await createIntegrationAsync(integration);
|
||||
return await integrationInstance.getTimetableAsync({
|
||||
stationId: input.stationId,
|
||||
limit: input.limit,
|
||||
});
|
||||
},
|
||||
queryKey: "timetableGetTimetable",
|
||||
cacheDuration: dayjs.duration(1, "minute"),
|
||||
});
|
||||
@@ -36,7 +36,7 @@
|
||||
"@mantine/core": "^8.3.18",
|
||||
"@mantine/hooks": "^8.3.18",
|
||||
"@mantine/spotlight": "^8.3.18",
|
||||
"@tabler/icons-react": "^3.40.0",
|
||||
"@tabler/icons-react": "^3.41.0",
|
||||
"jotai": "^2.19.0",
|
||||
"next": "16.2.1",
|
||||
"react": "19.2.4",
|
||||
|
||||
@@ -1125,17 +1125,17 @@
|
||||
"userAvatar": {
|
||||
"menu": {
|
||||
"switchToDarkMode": "Switch to dark mode",
|
||||
"switchToLightMode": "Switch to light mode",
|
||||
"management": "Management",
|
||||
"preferences": "Your preferences",
|
||||
"logout": "Log-out",
|
||||
"switchToLightMode": "التبديل إلى الوضع الفاتح",
|
||||
"management": "الإدارة",
|
||||
"preferences": "تفضيلاتك",
|
||||
"logout": "تسجيل الخروج",
|
||||
"login": "Log-in",
|
||||
"homeBoard": "Your home board",
|
||||
"homeBoard": "لوحة منزلك",
|
||||
"loggedOut": "Logged out",
|
||||
"updateAvailable": "New update available: {tag}"
|
||||
"updateAvailable": "{countUpdates} يتوفر تحديث جديد: {tag}"
|
||||
}
|
||||
},
|
||||
"dangerZone": "Danger zone",
|
||||
"dangerZone": "المنطقة الخطرة",
|
||||
"noResults": "No results have been found",
|
||||
"unsavedChanges": "You have unsaved changes!",
|
||||
"servers": "Servers",
|
||||
@@ -1627,7 +1627,7 @@
|
||||
"label": "Allow geolocation"
|
||||
},
|
||||
"allowModals": {
|
||||
"label": ""
|
||||
"label": "Allow modals"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -1859,110 +1859,110 @@
|
||||
"label": "Show CPU Info"
|
||||
},
|
||||
"memory": {
|
||||
"label": ""
|
||||
"label": "Show Memory Info"
|
||||
},
|
||||
"gpu": {
|
||||
"label": ""
|
||||
"label": "Show GPU Info"
|
||||
},
|
||||
"showUptime": {
|
||||
"label": ""
|
||||
"label": "Show Uptime"
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": ""
|
||||
"label": "Show Filesystem Info"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
"label": "Default tab"
|
||||
},
|
||||
"visibleClusterSections": {
|
||||
"label": ""
|
||||
"label": "Visible cluster sections"
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
"label": "Section indicator requirement"
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"information": "",
|
||||
"processor": "",
|
||||
"memory": "",
|
||||
"memoryAvailable": "",
|
||||
"version": "",
|
||||
"uptime": "",
|
||||
"loadAverage": "",
|
||||
"minute": "",
|
||||
"minutes": "",
|
||||
"used": "",
|
||||
"available": "",
|
||||
"lastSeen": ""
|
||||
"information": "Information",
|
||||
"processor": "Processor: {cpuModelName}",
|
||||
"memory": "Memory: {memory}GiB",
|
||||
"memoryAvailable": "Available: {memoryAvailable}GiB ({percent}%)",
|
||||
"version": "Version: {version}",
|
||||
"uptime": "Uptime: {months} Months, {days} Days, {hours} Hours, {minutes} Minutes",
|
||||
"loadAverage": "Load average:",
|
||||
"minute": "1 minute",
|
||||
"minutes": "{count} minutes",
|
||||
"used": "Used",
|
||||
"available": "Available",
|
||||
"lastSeen": "Last status update: {lastSeen}"
|
||||
},
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
"internalServerError": "Failed to fetch health status"
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
"cpu": "CPU",
|
||||
"memory": "RAM"
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
"name": "Nodes"
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
"name": "VMs"
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
"name": "LXCs"
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
"name": "Storage"
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
"node": "Node",
|
||||
"vmId": "VM ID",
|
||||
"plugin": "Plugin"
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"cpu": "Cores",
|
||||
"memory": "Memory",
|
||||
"storage": "Storage",
|
||||
"uptime": "Uptime",
|
||||
"haState": "HA State",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
"local": "Local storage",
|
||||
"shared": "Shared storage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
"name": "Name",
|
||||
"cpu": "CPU",
|
||||
"memory": "RAM",
|
||||
"node": "Node"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dockerContainers": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"name": "Docker stats",
|
||||
"description": "Stats of your containers (This widget can only be added with administrator privileges)",
|
||||
"option": {
|
||||
"enableRowSorting": {
|
||||
"label": ""
|
||||
"label": "Enable items sorting"
|
||||
},
|
||||
"defaultSort": {
|
||||
"label": "",
|
||||
"label": "Column used for sorting by default",
|
||||
"option": {
|
||||
"name": "",
|
||||
"state": "",
|
||||
"cpuUsage": "",
|
||||
"memoryUsage": ""
|
||||
"name": "Name",
|
||||
"state": "State",
|
||||
"cpuUsage": "CPU usage",
|
||||
"memoryUsage": "Memory usage"
|
||||
}
|
||||
},
|
||||
"descendingDefaultSort": {
|
||||
"label": ""
|
||||
"label": "Invert sorting"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -3157,98 +3157,98 @@
|
||||
},
|
||||
"navbar": {
|
||||
"items": {
|
||||
"home": "",
|
||||
"boards": "",
|
||||
"apps": "",
|
||||
"integrations": "",
|
||||
"searchEngies": "",
|
||||
"medias": "",
|
||||
"home": "الصفحة الرئسية",
|
||||
"boards": "الألواح",
|
||||
"apps": "التطبيقات",
|
||||
"integrations": "التكاملات",
|
||||
"searchEngies": "محركات البحث",
|
||||
"medias": "الوسائط",
|
||||
"users": {
|
||||
"label": "",
|
||||
"label": "المستخدمين",
|
||||
"items": {
|
||||
"manage": "",
|
||||
"invites": "",
|
||||
"groups": ""
|
||||
"manage": "إدارة المستخدمين",
|
||||
"invites": "الدعوات",
|
||||
"groups": "المجموعات"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"label": "",
|
||||
"label": "الأدوات",
|
||||
"items": {
|
||||
"docker": "",
|
||||
"docker": "الحاويات",
|
||||
"kubernetes": "",
|
||||
"logs": "",
|
||||
"api": "",
|
||||
"certificates": "",
|
||||
"tasks": ""
|
||||
"logs": "السجلات",
|
||||
"api": "API",
|
||||
"certificates": "الشهادات",
|
||||
"tasks": "المهام"
|
||||
}
|
||||
},
|
||||
"settings": "",
|
||||
"settings": "الإعدادات",
|
||||
"help": {
|
||||
"label": "",
|
||||
"label": "المساعدة",
|
||||
"items": {
|
||||
"documentation": "",
|
||||
"submitIssue": "",
|
||||
"discord": "",
|
||||
"sourceCode": ""
|
||||
"documentation": "المستندات",
|
||||
"submitIssue": "إبلاغ عن مشكلة",
|
||||
"discord": "مجتمع ديسكورد",
|
||||
"sourceCode": "كود المصدر"
|
||||
}
|
||||
},
|
||||
"about": ""
|
||||
"about": "حول"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"home": {
|
||||
"statistic": {
|
||||
"board": "",
|
||||
"user": "",
|
||||
"invite": "",
|
||||
"integration": "",
|
||||
"app": "",
|
||||
"group": "",
|
||||
"searchEngine": "",
|
||||
"media": ""
|
||||
"board": "الألواح",
|
||||
"user": "مستخدمين",
|
||||
"invite": "دعوات",
|
||||
"integration": "التكاملات",
|
||||
"app": "البرامج",
|
||||
"group": "المجموعات",
|
||||
"searchEngine": "محركات البحث",
|
||||
"media": "الوسائط"
|
||||
},
|
||||
"statisticLabel": {
|
||||
"boards": "",
|
||||
"resources": "",
|
||||
"authentication": "",
|
||||
"authorization": ""
|
||||
"boards": "اللوحات",
|
||||
"resources": "الموارد",
|
||||
"authentication": "المصادقة",
|
||||
"authorization": "الأذونات"
|
||||
},
|
||||
"heroBanner": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
"title": "مرحبًا بعودتك إلى",
|
||||
"subtitle": "{app} اللوحات"
|
||||
}
|
||||
},
|
||||
"board": {
|
||||
"title": "",
|
||||
"title": "لوحاتك",
|
||||
"action": {
|
||||
"new": {
|
||||
"label": ""
|
||||
"label": "لوحة جديدة"
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
"label": "فتح اللوحة"
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
"label": "الإعدادات"
|
||||
},
|
||||
"setHomeBoard": {
|
||||
"label": "",
|
||||
"label": "تعيينه كلوحة رئيسية",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"label": "الرئيسية",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"label": "تعيينه كصفحتك الرئيسية للجوال",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"label": "الجوال",
|
||||
"tooltip": ""
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
"label": "تكرار اللوحة"
|
||||
},
|
||||
"delete": {
|
||||
"label": "",
|
||||
"label": "حذف نهائيًا",
|
||||
"confirm": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
@@ -3337,10 +3337,10 @@
|
||||
}
|
||||
},
|
||||
"invite": {
|
||||
"title": "",
|
||||
"title": "إدارة دعوات المستخدمين",
|
||||
"action": {
|
||||
"new": {
|
||||
"title": "",
|
||||
"title": "دعوة جديدة",
|
||||
"description": ""
|
||||
},
|
||||
"copy": {
|
||||
@@ -3356,13 +3356,13 @@
|
||||
},
|
||||
"field": {
|
||||
"id": {
|
||||
"label": ""
|
||||
"label": "المعرّف"
|
||||
},
|
||||
"creator": {
|
||||
"label": ""
|
||||
"label": "المنشئ"
|
||||
},
|
||||
"expirationDate": {
|
||||
"label": ""
|
||||
"label": "انتهاء الصلاحية"
|
||||
},
|
||||
"token": {
|
||||
"label": ""
|
||||
@@ -4129,7 +4129,7 @@
|
||||
"manage": {
|
||||
"label": "",
|
||||
"boards": {
|
||||
"label": ""
|
||||
"label": "الألواح"
|
||||
},
|
||||
"integrations": {
|
||||
"label": "",
|
||||
@@ -4162,7 +4162,7 @@
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"label": "",
|
||||
"label": "المستخدمين",
|
||||
"create": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -4173,7 +4173,7 @@
|
||||
"label": ""
|
||||
},
|
||||
"invites": {
|
||||
"label": ""
|
||||
"label": "الدعوات"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
@@ -4230,7 +4230,7 @@
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "",
|
||||
"placeholder": "البحث عن",
|
||||
"nothingFound": "",
|
||||
"error": {
|
||||
"fetch": ""
|
||||
@@ -4390,13 +4390,13 @@
|
||||
"title": "",
|
||||
"option": {
|
||||
"documentation": {
|
||||
"label": ""
|
||||
"label": "Documentation"
|
||||
},
|
||||
"submitIssue": {
|
||||
"label": ""
|
||||
"label": "Submit an issue"
|
||||
},
|
||||
"discord": {
|
||||
"label": ""
|
||||
"label": "Community Discord"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4405,20 +4405,20 @@
|
||||
"home": {
|
||||
"group": {
|
||||
"search": {
|
||||
"title": "",
|
||||
"title": "Search",
|
||||
"option": {
|
||||
"other": {
|
||||
"label": ""
|
||||
"label": "Search with another search engine"
|
||||
},
|
||||
"no-default": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "No default search engine",
|
||||
"description": "Set a default search engine in preferences"
|
||||
},
|
||||
"search": {
|
||||
"label": ""
|
||||
"label": "Search for \"{query}\" with {name}"
|
||||
},
|
||||
"from-integration": {
|
||||
"description": ""
|
||||
"description": "Start typing to search"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1525,6 +1525,17 @@
|
||||
"offline": "Offline"
|
||||
}
|
||||
},
|
||||
"timetable": {
|
||||
"name": "Timetable",
|
||||
"description": "Displays departure times for a station",
|
||||
"title": "Departures {station}",
|
||||
"noStation": "No station selected",
|
||||
"option": {
|
||||
"station": {
|
||||
"label": "Station"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notebook": {
|
||||
"name": "Notebook",
|
||||
"description": "A simple notebook widget that supports markdown",
|
||||
@@ -3513,6 +3524,9 @@
|
||||
"minecraftServerStatus": {
|
||||
"label": "Minecraft server status"
|
||||
},
|
||||
"timetable": {
|
||||
"label": "Timetable"
|
||||
},
|
||||
"iconsUpdater": {
|
||||
"label": "Icons Updater"
|
||||
},
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
},
|
||||
"invite": {
|
||||
"title": "Rejoignez Homarr",
|
||||
"subtitle": "Bienvenue sur Homarr! Veuillez créer votre compte",
|
||||
"subtitle": "Bienvenue sur Homarr ! Veuillez créer votre compte",
|
||||
"description": "Vous avez été invité par {username}"
|
||||
},
|
||||
"init": {
|
||||
@@ -177,7 +177,7 @@
|
||||
}
|
||||
},
|
||||
"forgotPassword": {
|
||||
"label": "Mot de passe oublié?",
|
||||
"label": "Mot de passe oublié ?",
|
||||
"description": "Un administrateur peut utiliser la commande suivante pour réinitialiser votre mot de passe :"
|
||||
}
|
||||
},
|
||||
@@ -578,7 +578,7 @@
|
||||
},
|
||||
"delete": {
|
||||
"title": "Supprimer l'app",
|
||||
"message": "Êtes-vous sûr de vouloir supprimer l'app {name}?",
|
||||
"message": "Êtes-vous sûr de vouloir supprimer l'app {name} ?",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "Suppression effectuée avec succès",
|
||||
@@ -684,7 +684,7 @@
|
||||
},
|
||||
"delete": {
|
||||
"title": "Supprimer l'intégration",
|
||||
"message": "Êtes-vous sûr(e) de vouloir supprimer l'intégration {name}?",
|
||||
"message": "Êtes-vous sûr(e) de vouloir supprimer l'intégration {name} ?",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "Suppression réussie",
|
||||
@@ -742,7 +742,7 @@
|
||||
"description": "La réponse n'a pas pu être analysée. Veuillez vérifier que l'URL pointe vers l'URL de base du service."
|
||||
},
|
||||
"authorization": {
|
||||
"title": "",
|
||||
"title": "Erreur d'autorisation",
|
||||
"description": "La requête n'a pas été autorisée. Veuillez vérifier que les informations d'identification sont correctes et que vous les avez configurées avec suffisamment d'autorisations."
|
||||
},
|
||||
"statusCode": {
|
||||
@@ -794,7 +794,7 @@
|
||||
"hostnameMismatch": {
|
||||
"confirm": {
|
||||
"title": "Faire confiant à l'incohérence du nom d'hôte",
|
||||
"message": ""
|
||||
"message": "Êtes-vous sûr de vouloir faire confiance ce certificat avec un nom d'hôte incohérent ?"
|
||||
},
|
||||
"notification": {
|
||||
"success": {
|
||||
@@ -1164,8 +1164,8 @@
|
||||
"number": "Ce champ doit être inférieur ou égal à {maximum}"
|
||||
},
|
||||
"custom": {
|
||||
"passwordsDoNotMatch": "Les mots de passe ne correspondent pas.",
|
||||
"passwordRequirements": "Le mot de passe ne respecte pas les exigences requises.",
|
||||
"passwordsDoNotMatch": "Les mots de passe ne correspondent pas",
|
||||
"passwordRequirements": "Le mot de passe ne respecte pas les exigences requises",
|
||||
"boardAlreadyExists": "Un tableau avec ce nom existe déjà",
|
||||
"invalidFileType": "Type de fichier invalide, {expected} attendu",
|
||||
"invalidFileName": "Nom de fichier invalide",
|
||||
@@ -1248,7 +1248,7 @@
|
||||
"action": {
|
||||
"create": "Nouvel élément",
|
||||
"import": "Importer un élément",
|
||||
"edit": "Editer l'élément",
|
||||
"edit": "Éditer l'élément",
|
||||
"moveResize": "Déplacer / redimensionner l'élément",
|
||||
"duplicate": "Dupliquer l'élément",
|
||||
"remove": "Supprimer l'élément"
|
||||
@@ -1281,7 +1281,7 @@
|
||||
}
|
||||
},
|
||||
"edit": {
|
||||
"title": "Editer l'élément",
|
||||
"title": "Éditer l'élément",
|
||||
"advancedOptions": {
|
||||
"label": "Options avancées",
|
||||
"title": "Options avancées de l'élément"
|
||||
@@ -1309,7 +1309,7 @@
|
||||
"widget": {
|
||||
"app": {
|
||||
"name": "App",
|
||||
"description": "Intégre une application dans le tableau.",
|
||||
"description": "Intègre une application dans le tableau.",
|
||||
"option": {
|
||||
"appId": {
|
||||
"label": "Choisir une application"
|
||||
@@ -1637,7 +1637,7 @@
|
||||
}
|
||||
},
|
||||
"smartHome-entityState": {
|
||||
"name": "Etat de l'entité",
|
||||
"name": "État de l'entité",
|
||||
"description": "Affichez l'état d'une entité et optionnellement activez la",
|
||||
"option": {
|
||||
"entityId": {
|
||||
@@ -1883,16 +1883,16 @@
|
||||
"popover": {
|
||||
"information": "Information",
|
||||
"processor": "Processeur : {cpuModelName}",
|
||||
"memory": "Mémoire: {memory}GiB",
|
||||
"memoryAvailable": "Disponible: {memoryAvailable}GiB ({percent}%)",
|
||||
"memory": "Mémoire : {memory}GiB",
|
||||
"memoryAvailable": "Disponible : {memoryAvailable}GiB ({percent}%)",
|
||||
"version": "Version : {version}",
|
||||
"uptime": "Disponibilité: {months} Mois, {days} Jours, {hours} Heures, {minutes} Minutes",
|
||||
"loadAverage": "Charge moyenne:",
|
||||
"uptime": "Disponibilité : {months} Mois, {days} Jours, {hours} Heures, {minutes} Minutes",
|
||||
"loadAverage": "Charge moyenne :",
|
||||
"minute": "1 minute",
|
||||
"minutes": "{count} minutes",
|
||||
"used": "Utilisé ",
|
||||
"available": "Disponible",
|
||||
"lastSeen": "Dernière mise à jour de statut: {lastSeen}"
|
||||
"lastSeen": "Dernière mise à jour de statut : {lastSeen}"
|
||||
},
|
||||
"memory": {},
|
||||
"error": {
|
||||
@@ -1928,7 +1928,7 @@
|
||||
"memory": "Mémoire",
|
||||
"storage": "Stockage",
|
||||
"uptime": "Disponibilité",
|
||||
"haState": "Etat HA",
|
||||
"haState": "État HA",
|
||||
"storageType": {
|
||||
"local": "Stockage local",
|
||||
"shared": "Stockage partagé"
|
||||
@@ -2062,7 +2062,7 @@
|
||||
"query": "Ville / Code Postal",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"disabledTooltip": "Veuillez saisir une ville ou un code postal.",
|
||||
"disabledTooltip": "Veuillez saisir une ville ou un code postal",
|
||||
"unknownLocation": "Lieu inconnu",
|
||||
"search": "Rechercher",
|
||||
"table": {
|
||||
@@ -2081,7 +2081,7 @@
|
||||
}
|
||||
},
|
||||
"integration": {
|
||||
"noData": "Aucune intégration trouvée.",
|
||||
"noData": "Aucune intégration trouvée",
|
||||
"description": "Cliquez sur <here></here> pour créer une nouvelle intégration"
|
||||
},
|
||||
"app": {
|
||||
@@ -2097,7 +2097,7 @@
|
||||
},
|
||||
"video": {
|
||||
"name": "Flux vidéo",
|
||||
"description": "Intégre un flux vidéo ou une vidéo provenant d'une caméra ou d'un site web",
|
||||
"description": "Intègre un flux vidéo ou une vidéo provenant d'une caméra ou d'un site web",
|
||||
"option": {
|
||||
"feedUrl": {
|
||||
"label": "URL du flux"
|
||||
@@ -2110,7 +2110,7 @@
|
||||
"label": "Muet"
|
||||
},
|
||||
"hasControls": {
|
||||
"label": "Afficher les controles"
|
||||
"label": "Afficher les contrôles"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -2336,7 +2336,7 @@
|
||||
"pause": "Mettre en pause l'élément",
|
||||
"resume": "Reprendre l'élément",
|
||||
"delete": {
|
||||
"title": "Supprimer l'élement",
|
||||
"title": "Supprimer l'élément",
|
||||
"modalTitle": "Êtes-vous sûr de vouloir supprimer cette tâche ?",
|
||||
"entry": "Supprimer l'entrée",
|
||||
"entryAndFiles": "Supprimer l'entrée et le(s) fichier(s)"
|
||||
@@ -2548,7 +2548,7 @@
|
||||
"allImagesAlreadyImported": "Toutes les images déjà importées",
|
||||
"onlyAdminCanImport": "Seuls les administrateurs peuvent importer depuis docker",
|
||||
"selectAll": "Tout sélectionner",
|
||||
"deselectAll": "Tout déselectionner"
|
||||
"deselectAll": "Tout désélectionner"
|
||||
},
|
||||
"provider": {
|
||||
"label": "Fournisseur"
|
||||
@@ -2673,7 +2673,7 @@
|
||||
},
|
||||
"error": {
|
||||
"integrationsDisconnected": "Aucune donnée disponible, toutes les intégrations sont déconnectées",
|
||||
"unknownContentOption": "Option de contenu inconnue pour le widget d'état du réseau: "
|
||||
"unknownContentOption": "Option de contenu inconnue pour le widget d'état du réseau : "
|
||||
},
|
||||
"name": "État du réseau",
|
||||
"description": "Afficher les périphériques connectés sur un réseau"
|
||||
@@ -2765,7 +2765,7 @@
|
||||
"label": "Mode d'affichage",
|
||||
"option": {
|
||||
"percentage": {
|
||||
"label": "Pourcentage (ex : 70%)"
|
||||
"label": "Pourcentage (ex : 76%)"
|
||||
},
|
||||
"absolute": {
|
||||
"label": "Valeurs absolues (ex. : 800 Go / 1 To)"
|
||||
@@ -2973,7 +2973,7 @@
|
||||
},
|
||||
"customCss": {
|
||||
"label": "CSS personnalisé pour ce tableau",
|
||||
"description": "En outre, vous pouvez personnaliser votre tableau de bord à l'aide de CSS. Réservé aux utilisateurs expérimentés.",
|
||||
"description": "De plus, personnalisez votre tableau de bord à l'aide de CSS, recommandé uniquement pour les utilisateurs expérimentés",
|
||||
"customClassesAlert": {
|
||||
"title": "Classes personnalisées",
|
||||
"description": "Vous pouvez ajouter des classes personnalisées aux éléments de votre tableau dans les options avancées de chaque élément et les utiliser dans le CSS personnalisé ci-dessus."
|
||||
@@ -3251,7 +3251,7 @@
|
||||
"label": "Supprimer définitivement",
|
||||
"confirm": {
|
||||
"title": "Supprimer le tableau de bord",
|
||||
"description": "Êtes-vous sûr de vouloir supprimer le tableau {name}?"
|
||||
"description": "Êtes-vous sûr de vouloir supprimer le tableau {name} ?"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"title": "Import token megadása",
|
||||
"field": {
|
||||
"token": {
|
||||
"label": "",
|
||||
"label": "Token",
|
||||
"description": "Írja be a megjelenített import tokent az előző homarr példányból"
|
||||
}
|
||||
},
|
||||
@@ -153,10 +153,10 @@
|
||||
"label": "Ikonok használata a pingekhez"
|
||||
},
|
||||
"defaultSearchEngine": {
|
||||
"label": ""
|
||||
"label": "Alapértelmezett kereső"
|
||||
},
|
||||
"openSearchInNewTab": {
|
||||
"label": ""
|
||||
"label": "Nyissa meg a keresési eredményeket új ablakban"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -219,10 +219,10 @@
|
||||
"changeSearchPreferences": {
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "A keresési beállítások sikeresen megváltoztak"
|
||||
},
|
||||
"error": {
|
||||
"message": ""
|
||||
"message": "A keresési beállításokat nem sikerült megváltoztatni"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -307,12 +307,12 @@
|
||||
"name": "Név",
|
||||
"members": "Tagok",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Főoldal",
|
||||
"description": "Csak a csoport számára elérhető oldalak választhatók ki"
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Mobiloldal",
|
||||
"description": "Csak a csoport számára elérhető oldalak választhatók ki"
|
||||
}
|
||||
},
|
||||
"permission": {
|
||||
@@ -514,12 +514,12 @@
|
||||
"board": {
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Beállítások elmentve",
|
||||
"message": "Oldal beállítások sikeresen elmentve"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Nem sikerült menteni a beállításokat",
|
||||
"message": "Az oldalbeállítások nem menthetők"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -527,21 +527,21 @@
|
||||
"changePosition": {
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "Pozíció sikeresen megváltozott"
|
||||
},
|
||||
"error": {
|
||||
"message": ""
|
||||
"message": "Pozíció nem módosítható"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultGroup": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
"name": "Alapértelmezett csoport",
|
||||
"description": "{name} - Minden bejelentkezett felhasználó"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"search": "",
|
||||
"search": "Alkalmazás megtalálása",
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "Alkalmazások",
|
||||
@@ -599,12 +599,12 @@
|
||||
"label": "Leírás"
|
||||
},
|
||||
"url": {
|
||||
"label": ""
|
||||
"label": "Url"
|
||||
},
|
||||
"useDifferentUrlForPing": {
|
||||
"checkbox": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Más URL használata a ping-hez",
|
||||
"description": "Hasznos, ha a Homarr közvetlenül, egy belső hosztnév vagy hálózaton keresztül fér hozzá, hogy elkerülje az internetszolgáltató által használt sávszélesség terhelését"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -612,17 +612,17 @@
|
||||
"select": {
|
||||
"label": "Alkalmazás kiválasztása",
|
||||
"notFound": "Nem található alkalmazás",
|
||||
"search": "",
|
||||
"noResults": "",
|
||||
"action": "",
|
||||
"title": ""
|
||||
"search": "Alkalmazás keresése",
|
||||
"noResults": "Nincs találat",
|
||||
"action": "Kiválasztás {app}",
|
||||
"title": "Válassz egy programot az oldalhoz adáshoz"
|
||||
},
|
||||
"create": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"action": ""
|
||||
"title": "Új alkalmazás készítése",
|
||||
"description": "Új alkalmazás létrehozása ",
|
||||
"action": "Alkalmazás készítő megnyitása"
|
||||
},
|
||||
"add": ""
|
||||
"add": "Alkalmazás hozzáadása"
|
||||
}
|
||||
},
|
||||
"integration": {
|
||||
@@ -649,14 +649,14 @@
|
||||
"app": {
|
||||
"option": {
|
||||
"existing": {
|
||||
"title": "",
|
||||
"label": ""
|
||||
"title": "Meglévő",
|
||||
"label": "Meglévő alkalmazás kiválasztása"
|
||||
},
|
||||
"new": {
|
||||
"title": "",
|
||||
"title": "Új",
|
||||
"url": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Alkalmazás url",
|
||||
"description": "Az url, amelyre az alkalmazás a műszerfalról történő eléréskor irányít"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -676,9 +676,9 @@
|
||||
},
|
||||
"app": {
|
||||
"action": {
|
||||
"add": "",
|
||||
"remove": "",
|
||||
"select": ""
|
||||
"add": "Alkalmazás megtalálása",
|
||||
"remove": "Leválasztás",
|
||||
"select": "Válasszon ki egy alkalmazást a hivatkozáshoz"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -702,21 +702,21 @@
|
||||
"label": "Név"
|
||||
},
|
||||
"url": {
|
||||
"label": ""
|
||||
"label": "Url"
|
||||
},
|
||||
"attemptSearchEngineCreation": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Keresőmotor létrehozása",
|
||||
"description": "A \"{kind}\" integráció használható a keresőmotorokkal. Jelölje be ezt a jelölőnégyzetet, ha automatikusan szeretné konfigurálni a keresőmotort."
|
||||
},
|
||||
"app": {
|
||||
"sectionTitle": ""
|
||||
"sectionTitle": "Hivattkozott alkalmazás"
|
||||
},
|
||||
"createApp": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Alkalmazás létrehozása",
|
||||
"description": "Hozzon létre egy alkalmazást, amelynek ugyanaz a neve és ikonja van, mint az integrációnak. Hagyja üresen az alábbi beviteli mezőt, hogy az alkalmazás az integrációs URL-lel jöjjön létre."
|
||||
},
|
||||
"appHref": {
|
||||
"placeholder": ""
|
||||
"placeholder": "Egyéni alkalmazás URL"
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
@@ -730,59 +730,59 @@
|
||||
"error": {
|
||||
"common": {
|
||||
"cause": {
|
||||
"title": ""
|
||||
"title": "Részletesebb leírás a problémáról"
|
||||
}
|
||||
},
|
||||
"unknown": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
"title": "Ismeretlen hiba",
|
||||
"description": "Egy ismeretlen hiba történt, nyissa meg a részleteket alul, hogy többet megtudjon"
|
||||
},
|
||||
"parse": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
"title": "Elemzési hiba",
|
||||
"description": "A válasz nem volt elemezhető. Kérjük, ellenőrizze, hogy az URL a szolgáltatás alap URL-jére mutat-e."
|
||||
},
|
||||
"authorization": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
"title": "Hitelesítési hiba",
|
||||
"description": "A kérés nem volt engedélyezett. Kérjük, ellenőrizze, hogy a hitelesítő adatok helyesek-e, és hogy a megfelelő engedélyekkel vannak-e beállítva."
|
||||
},
|
||||
"statusCode": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"otherDescription": "",
|
||||
"title": "Válaszhiba",
|
||||
"description": "Váratlan {statusCode} ({reason}) választ kapott a <url></url> címen. Kérjük, ellenőrizze, hogy az URL az integráció alap URL-jére mutat-e.",
|
||||
"otherDescription": "Váratlan {statusCode} választ kapott a <url></url> címen. Kérjük, ellenőrizze, hogy az URL az integráció alap URL-jére mutat-e.",
|
||||
"reason": {
|
||||
"badRequest": "",
|
||||
"notFound": "",
|
||||
"tooManyRequests": "",
|
||||
"internalServerError": "",
|
||||
"serviceUnavailable": "",
|
||||
"gatewayTimeout": ""
|
||||
"badRequest": "Hibás kérés",
|
||||
"notFound": "Nem található",
|
||||
"tooManyRequests": "Túl sok kérés",
|
||||
"internalServerError": "Belső szerverhiba",
|
||||
"serviceUnavailable": "A szolgáltatás nem elérhető",
|
||||
"gatewayTimeout": "Átjáró időtúllépés"
|
||||
}
|
||||
},
|
||||
"certificate": {
|
||||
"title": "",
|
||||
"title": "Tanúsítványhiba",
|
||||
"description": {
|
||||
"expired": "",
|
||||
"notYetValid": "",
|
||||
"untrusted": "",
|
||||
"hostnameMismatch": ""
|
||||
"expired": "A tanúsítvány lejárt.",
|
||||
"notYetValid": "A tanúsítvány még nem érvényes.",
|
||||
"untrusted": "A tanúsítvány nem megbízható.",
|
||||
"hostnameMismatch": "A tanúsítványban szereplő hosztnév nem egyezik meg az URL-lel."
|
||||
},
|
||||
"alert": {
|
||||
"permission": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Nincsenek megfelelő engedélyek",
|
||||
"message": "Önnek nincs joga tanúsítványokat megbízhatónak nyilvánítani vagy feltölteni. Kérjük, lépjen kapcsolatba a rendszergazdájával, hogy feltölthesse a szükséges gyökér tanúsítványt."
|
||||
},
|
||||
"hostnameMismatch": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Állomásnév eltérés",
|
||||
"message": "A tanúsítványban szereplő hosztnév nem egyezik meg azzal a hosztnévvel, amelyhez kapcsolódik. Ez biztonsági kockázatot jelezhet, de mégis dönthet úgy, hogy megbízik ebben a tanúsítványban."
|
||||
},
|
||||
"extract": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "A tanúsító hatóság (CA) tanúsítványának kinyerése sikertelen",
|
||||
"message": "Csak a lánc nélküli, önaláírt tanúsítványok tölthetők le automatikusan. Ha önaláírt tanúsítványt használ, kérjük, feltétlenül töltse fel manuálisan a tanúsító hatóság (CA) tanúsítványát. Az ehhez szükséges lépések leírását itt találja: <docsLink></docsLink>."
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"retry": {
|
||||
"label": ""
|
||||
"label": "Újrapróbál"
|
||||
},
|
||||
"trust": {
|
||||
"label": ""
|
||||
@@ -920,8 +920,8 @@
|
||||
"message": "Az útvonal valószínűleg nem helyes"
|
||||
},
|
||||
"tooManyRequests": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Túl sok kérés adott idő alatt",
|
||||
"message": "Túl sok kérést küldött. Valószínűleg a célrendszer korlátozza vagy elutasítja a kéréseket"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -955,36 +955,36 @@
|
||||
"newLabel": "Új jelszó"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
"label": "Token azonosító",
|
||||
"newLabel": "Új token azonosító"
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
"label": "Tartomány",
|
||||
"newLabel": "Új tartomány"
|
||||
},
|
||||
"personalAccessToken": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
"label": "Személyes Hozzáférési Token",
|
||||
"newLabel": "Új Személyes Hozzáférési Token"
|
||||
},
|
||||
"topic": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
"label": "Téma",
|
||||
"newLabel": "Új téma"
|
||||
},
|
||||
"url": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
"label": "Url",
|
||||
"newLabel": "Új url"
|
||||
},
|
||||
"opnsenseApiKey": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
"label": "API kulcs (Kulcs)",
|
||||
"newLabel": "Új API Kulcs (Kulcs)"
|
||||
},
|
||||
"opnsenseApiSecret": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
"label": "Api Kulcs (Titkos)",
|
||||
"newLabel": "Új API Kulcs (Titkos)"
|
||||
},
|
||||
"githubAppId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
"label": "App Azonosító",
|
||||
"newLabel": "Új App Azonosító"
|
||||
},
|
||||
"githubInstallationId": {
|
||||
"label": "",
|
||||
|
||||
@@ -1627,7 +1627,7 @@
|
||||
"label": "Permitir geolocalização"
|
||||
},
|
||||
"allowModals": {
|
||||
"label": ""
|
||||
"label": "Permitir modais"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -1862,7 +1862,7 @@
|
||||
"label": "Mostrar informações da memória"
|
||||
},
|
||||
"gpu": {
|
||||
"label": ""
|
||||
"label": "Mostrar informações da GPU"
|
||||
},
|
||||
"showUptime": {
|
||||
"label": "Exibir tempo de atividade"
|
||||
@@ -2003,58 +2003,58 @@
|
||||
}
|
||||
},
|
||||
"tracearr": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"name": "Tracearr",
|
||||
"description": "Monitoramento avançado de servidor de mídia com dados detalhados de transmissão e transcodificação\n",
|
||||
"option": {
|
||||
"showStreams": {
|
||||
"label": ""
|
||||
"label": "Exibir transmissões ativas"
|
||||
},
|
||||
"showStats": {
|
||||
"label": ""
|
||||
"label": "Exibir resumo de estatísticas"
|
||||
},
|
||||
"showRecentActivity": {
|
||||
"label": ""
|
||||
"label": "Exibir Atividade Recente"
|
||||
},
|
||||
"showViolations": {
|
||||
"label": ""
|
||||
"label": "Mostrar Violações"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"activeStreams": "",
|
||||
"totalUsers": "",
|
||||
"transcodes": "",
|
||||
"bandwidth": ""
|
||||
"activeStreams": "Streams Ativas",
|
||||
"totalUsers": "Total de usuários",
|
||||
"transcodes": "Transcodificações",
|
||||
"bandwidth": "Largura da banda"
|
||||
},
|
||||
"streams": {
|
||||
"empty": "",
|
||||
"user": "",
|
||||
"media": "",
|
||||
"state": "",
|
||||
"quality": "",
|
||||
"device": "",
|
||||
"progress": "",
|
||||
"empty": "Sem transmissões ativas",
|
||||
"user": "Usuário",
|
||||
"media": "Mídia",
|
||||
"state": "Estado",
|
||||
"quality": "Qualidade",
|
||||
"device": "Dispositivo",
|
||||
"progress": "Progresso",
|
||||
"videoDecision": {
|
||||
"directPlay": "",
|
||||
"transcode": "",
|
||||
"directStream": ""
|
||||
"directPlay": "Reprodução direta",
|
||||
"transcode": "Transcodificação",
|
||||
"directStream": "Transmissão direta"
|
||||
}
|
||||
},
|
||||
"recentActivity": {
|
||||
"title": "",
|
||||
"empty": "",
|
||||
"watched": "",
|
||||
"partial": ""
|
||||
"title": "Atividade recente",
|
||||
"empty": "Nenhuma atividade recente",
|
||||
"watched": "Assistido",
|
||||
"partial": "Parcial"
|
||||
},
|
||||
"violations": {
|
||||
"title": "",
|
||||
"empty": "",
|
||||
"rule": "",
|
||||
"user": ""
|
||||
"title": "Violações",
|
||||
"empty": "Não há violações recentes",
|
||||
"rule": "Regra",
|
||||
"user": "Usuário"
|
||||
},
|
||||
"noSectionsEnabled": "",
|
||||
"noSectionsEnabled": "Nenhuma seção habilitada",
|
||||
"error": {
|
||||
"noIntegration": "",
|
||||
"internalServerError": ""
|
||||
"noIntegration": "Nenhuma integração Coolify configurada",
|
||||
"internalServerError": "Falha ao buscar dados do Tracearr"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
@@ -2153,50 +2153,50 @@
|
||||
}
|
||||
},
|
||||
"immich-serverStats": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"name": "Estatísticas do Servidor Immich",
|
||||
"description": "Informações sobre sua instância do Immich",
|
||||
"option": {
|
||||
"showUsers": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Mostrar usuários",
|
||||
"description": "Mostrar a contagem de usuários na sua instância"
|
||||
},
|
||||
"showPhotos": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Mostrar Fotos",
|
||||
"description": "Mostrar a contagem de fotos na sua instância"
|
||||
},
|
||||
"showVideos": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Mostrar vídeos",
|
||||
"description": "Mostrar contagem de vídeos na sua instância"
|
||||
},
|
||||
"showStorage": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Mostrar armazenamento",
|
||||
"description": "Mostrar a quantidade total de armazenamento utilizado"
|
||||
}
|
||||
},
|
||||
"users": "",
|
||||
"photos": "",
|
||||
"videos": "",
|
||||
"storage": ""
|
||||
"users": "Usuários",
|
||||
"photos": "Fotos",
|
||||
"videos": "Vídeos",
|
||||
"storage": "Armazenamento Utilizado"
|
||||
},
|
||||
"immich-albumCarousel": {
|
||||
"name": "",
|
||||
"name": "Álbum do Immich",
|
||||
"option": {
|
||||
"albumId": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "ID do Álbum",
|
||||
"description": "Esta é uma identificação única que se refere ao seu álbum Immich. Você pode encontrar essa identificação abrindo um álbum e copiando a string única da URL, por exemplo 'a21aca09-9f05-47dc-b1a1-7b53c547226e'"
|
||||
},
|
||||
"rotationIntervalSeconds": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Intervalo em segundos",
|
||||
"description": "Girar entre as imagens no álbum a cada X segundos. Deve ser pelo menos 1. Deve estar abaixo de 3600."
|
||||
},
|
||||
"showPhotoInfo": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Mostrar informações da foto",
|
||||
"description": "Exibir a data da imagem e o índice atual na parte inferior da apresentação"
|
||||
}
|
||||
},
|
||||
"description": "",
|
||||
"noAlbumSelected": "",
|
||||
"noPhotos": ""
|
||||
"description": "Apresentação de imagens em seu álbum Imich",
|
||||
"noAlbumSelected": "Nenhum álbum selecionado, edite o widget e escolha um álbum",
|
||||
"noPhotos": "Nenhuma foto neste álbum"
|
||||
},
|
||||
"downloads": {
|
||||
"name": "Cliente de Download",
|
||||
@@ -2731,7 +2731,7 @@
|
||||
"option": {
|
||||
"cpu": "CPU",
|
||||
"memory": "Memória",
|
||||
"gpu": "",
|
||||
"gpu": "GPU",
|
||||
"network": "Rede"
|
||||
}
|
||||
},
|
||||
@@ -2748,7 +2748,7 @@
|
||||
"card": {
|
||||
"cpu": "CPU",
|
||||
"memory": "MEM",
|
||||
"gpu": "",
|
||||
"gpu": "GPU",
|
||||
"network": "REDE",
|
||||
"up": "ENVIO",
|
||||
"down": "RECEBIDO"
|
||||
@@ -3583,7 +3583,7 @@
|
||||
"label": "Clima"
|
||||
},
|
||||
"tracearr": {
|
||||
"label": ""
|
||||
"label": "Tracearr"
|
||||
}
|
||||
},
|
||||
"interval": {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@mantine/core": "^8.3.18",
|
||||
"@mantine/dates": "^8.3.18",
|
||||
"@mantine/hooks": "^8.3.18",
|
||||
"@tabler/icons-react": "^3.40.0",
|
||||
"@tabler/icons-react": "^3.41.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "16.2.1",
|
||||
|
||||
@@ -51,23 +51,23 @@
|
||||
"@mantine/charts": "^8.3.18",
|
||||
"@mantine/core": "^8.3.18",
|
||||
"@mantine/hooks": "^8.3.18",
|
||||
"@tabler/icons-react": "^3.40.0",
|
||||
"@tiptap/extension-color": "3.20.5",
|
||||
"@tiptap/extension-highlight": "3.20.5",
|
||||
"@tiptap/extension-image": "3.20.5",
|
||||
"@tiptap/extension-link": "^3.20.5",
|
||||
"@tiptap/extension-placeholder": "^3.20.5",
|
||||
"@tiptap/extension-table": "3.20.5",
|
||||
"@tiptap/extension-table-cell": "3.20.5",
|
||||
"@tiptap/extension-table-header": "3.20.5",
|
||||
"@tiptap/extension-table-row": "3.20.5",
|
||||
"@tiptap/extension-task-item": "3.20.5",
|
||||
"@tiptap/extension-task-list": "3.20.5",
|
||||
"@tiptap/extension-text-align": "3.20.5",
|
||||
"@tiptap/extension-text-style": "3.20.5",
|
||||
"@tiptap/extension-underline": "3.20.5",
|
||||
"@tiptap/react": "^3.20.5",
|
||||
"@tiptap/starter-kit": "^3.20.5",
|
||||
"@tabler/icons-react": "^3.41.0",
|
||||
"@tiptap/extension-color": "3.21.0",
|
||||
"@tiptap/extension-highlight": "3.21.0",
|
||||
"@tiptap/extension-image": "3.21.0",
|
||||
"@tiptap/extension-link": "^3.21.0",
|
||||
"@tiptap/extension-placeholder": "^3.21.0",
|
||||
"@tiptap/extension-table": "3.21.0",
|
||||
"@tiptap/extension-table-cell": "3.21.0",
|
||||
"@tiptap/extension-table-header": "3.21.0",
|
||||
"@tiptap/extension-table-row": "3.21.0",
|
||||
"@tiptap/extension-task-item": "3.21.0",
|
||||
"@tiptap/extension-task-list": "3.21.0",
|
||||
"@tiptap/extension-text-align": "3.21.0",
|
||||
"@tiptap/extension-text-style": "3.21.0",
|
||||
"@tiptap/extension-underline": "3.21.0",
|
||||
"@tiptap/react": "^3.21.0",
|
||||
"@tiptap/starter-kit": "^3.21.0",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.20",
|
||||
"mantine-form-zod-resolver": "^1.3.0",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { WidgetOptionType } from "../options";
|
||||
import { WidgetAppInput } from "./widget-app-input";
|
||||
import { WidgetDynamicSelectInput } from "./widget-dynamic-select-input";
|
||||
import { WidgetLocationInput } from "./widget-location-input";
|
||||
import { WidgetMultiTextInput } from "./widget-multi-text-input";
|
||||
import { WidgetMultiReleasesRepositoriesInput } from "./widget-multiReleasesRepositories-input";
|
||||
@@ -23,6 +24,7 @@ const mapping = {
|
||||
app: WidgetAppInput,
|
||||
sortableItemList: WidgetSortedItemListInput,
|
||||
multiReleasesRepositories: WidgetMultiReleasesRepositoriesInput,
|
||||
dynamicSelect: WidgetDynamicSelectInput,
|
||||
} satisfies Record<WidgetOptionType, unknown>;
|
||||
|
||||
export const getInputForType = <TType extends WidgetOptionType>(type: TType) => {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Group, Loader, Select } from "@mantine/core";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
|
||||
import { translateIfNecessary } from "@homarr/translation";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
export interface DynamicSelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const WidgetDynamicSelectInput = ({ property, kind, options }: CommonWidgetInputProps<"dynamicSelect">) => {
|
||||
const t = useI18n();
|
||||
const tWidget = useWidgetInputTranslation(kind, property);
|
||||
const form = useFormContext();
|
||||
const inputProps = form.getInputProps(`options.${property}`);
|
||||
const [search, setSearch] = useState("");
|
||||
const [debouncedSearch] = useDebouncedValue(search, 300);
|
||||
const { isPending, options: selectOptions } = options.useOptions(debouncedSearch, form.values.integrationIds);
|
||||
const value = inputProps.value as DynamicSelectOption | null;
|
||||
const onChange = inputProps.onChange as (value: DynamicSelectOption | null) => void;
|
||||
|
||||
return (
|
||||
<Select
|
||||
label={tWidget("label")}
|
||||
data={selectOptions
|
||||
.map((option) =>
|
||||
typeof option === "string"
|
||||
? option
|
||||
: {
|
||||
value: option.value,
|
||||
label: translateIfNecessary(t, option.label) ?? option.value,
|
||||
},
|
||||
)
|
||||
.concat(
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!selectOptions.some((option) =>
|
||||
typeof option === "string" ? option === value.value : option.value === value.value,
|
||||
)
|
||||
? {
|
||||
value: value.value,
|
||||
label: value.label,
|
||||
}
|
||||
: [],
|
||||
)}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
leftSection={isPending && <Loader size="xs" />}
|
||||
renderOption={({ option, checked }) => {
|
||||
return (
|
||||
<Group flex="1" gap="xs">
|
||||
{option.label}
|
||||
{checked && (
|
||||
<IconCheck
|
||||
style={{ marginInlineStart: "auto" }}
|
||||
color="currentColor"
|
||||
opacity={0.6}
|
||||
size={18}
|
||||
stroke={1.5}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}}
|
||||
description={options.withDescription ? tWidget("description") : undefined}
|
||||
searchable
|
||||
{...inputProps}
|
||||
value={value?.value ?? null}
|
||||
onChange={(value) =>
|
||||
onChange(
|
||||
value
|
||||
? {
|
||||
value,
|
||||
label:
|
||||
selectOptions.filter((option) => typeof option === "object").find((option) => option.value === value)
|
||||
?.label ?? value,
|
||||
}
|
||||
: null,
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -42,6 +42,7 @@ import * as smartHomeExecuteAutomation from "./smart-home/execute-automation";
|
||||
import * as stockPrice from "./stocks";
|
||||
import * as systemDisks from "./system-disks";
|
||||
import * as systemResources from "./system-resources";
|
||||
import * as timetable from "./timetable";
|
||||
import * as tracearr from "./tracearr";
|
||||
import * as video from "./video";
|
||||
import * as weather from "./weather";
|
||||
@@ -82,6 +83,7 @@ export const widgetImports = {
|
||||
systemResources,
|
||||
coolify,
|
||||
systemDisks,
|
||||
timetable,
|
||||
"immich-serverStats": immichServerStats,
|
||||
"immich-albumCarousel": immichAlbumCarousel,
|
||||
tracearr,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ZodType } from "zod/v4";
|
||||
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
|
||||
import type { DynamicSelectOption } from "./_inputs/widget-dynamic-select-input";
|
||||
import type { inferSelectOptionValue, SelectOption } from "./_inputs/widget-select-input";
|
||||
import type { ReleasesRepository } from "./releases/releases-repository";
|
||||
|
||||
@@ -47,6 +48,16 @@ interface SelectInput<TOptions extends readonly SelectOption[]> extends CommonIn
|
||||
searchable?: boolean;
|
||||
}
|
||||
|
||||
interface DynamicSelectInput extends CommonInput<DynamicSelectOption | null> {
|
||||
useOptions: (
|
||||
query: string,
|
||||
integrationIds: string[],
|
||||
) => {
|
||||
isPending: boolean;
|
||||
options: DynamicSelectOption[];
|
||||
};
|
||||
}
|
||||
|
||||
interface NumberInput extends CommonInput<number> {
|
||||
validate: z.ZodNumber;
|
||||
step?: number;
|
||||
@@ -82,6 +93,12 @@ const optionsFactory = {
|
||||
searchable: input.searchable ?? false,
|
||||
withDescription: input.withDescription ?? false,
|
||||
}),
|
||||
dynamicSelect: (input: DynamicSelectInput) => ({
|
||||
type: "dynamicSelect" as const,
|
||||
defaultValue: input.defaultValue ?? null,
|
||||
useOptions: input.useOptions,
|
||||
withDescription: input.withDescription ?? false,
|
||||
}),
|
||||
select: <const TOptions extends SelectOption[]>(input: SelectInput<TOptions>) => ({
|
||||
type: "select" as const,
|
||||
defaultValue: (input.defaultValue ?? input.options[0]) as inferSelectOptionValue<TOptions[number]>,
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Badge, Center, Group, Stack, Text } from "@mantine/core";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { DynamicSelectOption } from "../_inputs/widget-dynamic-select-input";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
|
||||
export default function TimetableWidget({ options, integrationIds }: WidgetComponentProps<"timetable">) {
|
||||
// It will always have at least one integration as otherwise the NoIntegrationSelectedError would be thrown in item-content.tsx
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const integrationId = integrationIds[0]!;
|
||||
const t = useScopedI18n("widget.timetable");
|
||||
|
||||
if (!options.station) {
|
||||
return <Center h="100%">{t("noStation")}</Center>;
|
||||
}
|
||||
|
||||
return <TimetableWidgetInner station={options.station} integrationId={integrationId} />;
|
||||
}
|
||||
|
||||
interface TimetableWidgetInnerProps {
|
||||
station: DynamicSelectOption;
|
||||
integrationId: string;
|
||||
}
|
||||
|
||||
const TimetableWidgetInner = ({ station, integrationId }: TimetableWidgetInnerProps) => {
|
||||
const [timetable] = clientApi.widget.timetable.getTimetable.useSuspenseQuery({
|
||||
integrationId,
|
||||
stationId: station.value,
|
||||
limit: 10,
|
||||
});
|
||||
const t = useScopedI18n("widget.timetable");
|
||||
|
||||
return (
|
||||
<Stack w="100%" gap="xs" p="sm">
|
||||
<Text fw="bold">{t("title", { station: station.label })}</Text>
|
||||
{timetable.entries.map((entry) => (
|
||||
<Group key={`${entry.timestamp.toISOString()}-${entry.location}`} justify="space-between" w="100%">
|
||||
<Group gap="sm">
|
||||
{entry.line && (
|
||||
<Badge
|
||||
size="sm"
|
||||
color={entry.line.color ?? undefined}
|
||||
variant={entry.line.color ? "filled" : "default"}
|
||||
w={32}
|
||||
p={0}
|
||||
radius={0}
|
||||
>
|
||||
{entry.line.name}
|
||||
</Badge>
|
||||
)}
|
||||
<Text size="sm">
|
||||
{dayjs(entry.timestamp).format("HH:mm")}{" "}
|
||||
{entry.delay >= 1 && (
|
||||
<Text size="sm" span c="red">
|
||||
+{entry.delay}'
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Text size="sm">{entry.location}</Text>
|
||||
</Group>
|
||||
|
||||
{entry.platform && (
|
||||
<Text size="sm" c={entry.platform.hasChanged ? "red" : undefined}>
|
||||
{entry.platform.name}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { IconBusStop } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
export const { componentLoader, definition } = createWidgetDefinition("timetable", {
|
||||
icon: IconBusStop,
|
||||
supportedIntegrations: ["searchCh"],
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
station: factory.dynamicSelect({
|
||||
useOptions(query, integrationIds) {
|
||||
const { data: stations, isPending } = clientApi.widget.timetable.searchStations.useQuery(
|
||||
{
|
||||
integrationId: integrationIds[0] ?? "",
|
||||
query,
|
||||
},
|
||||
{
|
||||
enabled: integrationIds.length > 0,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
isPending,
|
||||
options:
|
||||
stations?.map((station) => ({
|
||||
label: station.name,
|
||||
value: station.id,
|
||||
})) ?? [],
|
||||
};
|
||||
},
|
||||
}),
|
||||
}));
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
Generated
+429
-450
File diff suppressed because it is too large.
Load diff
@@ -1 +1 @@
|
||||
[{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"tancak","avatar_url":"https://avatars.githubusercontent.com/u/11760749?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"alotlikebeans","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontent.com/uLine truncated
|
||||
[{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"tancak","avatar_url":"https://avatars.githubusercontent.com/u/11760749?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"alotlikebeans","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontent.com/uLine truncated
|
||||
@@ -1 +1 @@
|
||||
[{"username":"carlchina","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/6697/medium/d22bbe7797bbeb30dbdc73a5648d329a_default.png"},{"username":"zielmann","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png"},{"username":"Thalyn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12494028/medium/5faf5acc945a874ed116c4567edaa09f_default.png"},{"username":"magnushj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12503488/medium/68f32e33ea7b891960055c9f831f7344_default.png"},{"username":"almontegil","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12580457/medium/f4136cacbdfdb4c28ae7f85dc5f840db_default.png"},{"username":"andibing","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12664938/medium/b8be63e4dcb2e791ced1ffc9e3a049a5.jpg"},{"username":"gronare","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12680911/medium/c6eb14d1504156f34a158e507f73aa3d_default.png"},{"username":"SmartPhoneLover","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12701640/medium/ec95486662ec875cda080e778c3ff702.jpg"},{"username":"marcelotk_15","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12773227/medium/ecf6bea16ab0c873c131df2154ee9718_default.png"},{"username":"S3OD177","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13051544/medium/11dd990837f61f7e8f7d36b041a7fddb_default.png"},{"username":"marcg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13153301/medium/ebcf60fbf27deb148ec40a659566c6ff_default.png"},{"username":"BeersTeddy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13185230/medium/2f1f4e1effe74a23422b195cbefb2a95_default.png"},{"username":"jamesmcmahon0","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13245578/medium/586aa873b4abddbd9abc6f3de99ab70e.jpeg"},{"username":"b1nar10","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13286752/medium/249b02e3368752cb175446082015737c_default.png"},{"username":"vannCN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13330448/medium/9b8a9ee3611e51e951e22d5fd4eb7d8d.jpg"},{"username":"binswm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13343482/medium/45dde0e6097b9b72705d2eba9dbbc276_default.png"},{"username":"SkewRam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13496556/medium/188f5c2deb7938eda51eb786cc4539ca.jpeg"},{"username":"raphcatarino","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13547726/medium/b003511e67df13a4b4b5689488fa8099.jpg"},{"username":"shillos5","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13600557/medium/b17650b2b84f829d54fa548c44c1ab27.jpeg"},{"username":"wolong98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13641407/medium/f4634edc58c7857a357e5293543c15cf.jpg"},{"username":"spair0039","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14012333/medium/72430e96027c09c19141cac38eae4617.png"},{"username":"MajorMarcin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14072183/medium/0a197e64bb92b6ea94cecbb607512c45.jpg"},{"username":"KosmoMoustache","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14103535/medium/51c438cdd9f86e8811c75f4869de479c.png"},{"username":"Soochaehwa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14118689/medium/496a1ce63111547bf455a1e0a7ac75f1_default.png"},{"username":"garryfield","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14187035/medium/520c964706676045f6eeec1b08edc880.jpg"},{"username":"droidenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14444264/medium/4aa3a8e824f72bc9e5ec0d8de307095e.jpeg"},{"username":"maathis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14489950/medium/6eeddf13a757ee35f78f1763f94d95e1.jpeg"},{"username":"bukvam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14546502/medium/a819dc40a13c57395a17dc46e847d3ce.jpg"},{"username":"gm.cinalli","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14670666/medium/2d466a0fdbda40764526be86c97c0ab4.jpeg"},{"username":"antoine2tt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14722148/medium/f88d926900862dd59007ea4b3419cb9d.png"},{"username":"cretzen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14799754/medium/3ba5c1805ec20c000d5810c07985afe9.jpg"},{"username":"ktKongTong","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14817246/medium/97cfc3c028dbdaf85ebd1102da71e58c.jpeg"},{"username":"marinkaberg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14861042/medium/2d5c4e62613f03082f3e645fa92efd59.jpeg"},{"username":"djismgaming","avatarUrl":"httpsLine truncated
|
||||
[{"username":"carlchina","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/6697/medium/d22bbe7797bbeb30dbdc73a5648d329a_default.png"},{"username":"zielmann","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png"},{"username":"Thalyn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12494028/medium/5faf5acc945a874ed116c4567edaa09f_default.png"},{"username":"magnushj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12503488/medium/68f32e33ea7b891960055c9f831f7344_default.png"},{"username":"almontegil","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12580457/medium/f4136cacbdfdb4c28ae7f85dc5f840db_default.png"},{"username":"andibing","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12664938/medium/b8be63e4dcb2e791ced1ffc9e3a049a5.jpg"},{"username":"gronare","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12680911/medium/c6eb14d1504156f34a158e507f73aa3d_default.png"},{"username":"SmartPhoneLover","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12701640/medium/ec95486662ec875cda080e778c3ff702.jpg"},{"username":"marcelotk_15","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12773227/medium/ecf6bea16ab0c873c131df2154ee9718_default.png"},{"username":"S3OD177","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13051544/medium/11dd990837f61f7e8f7d36b041a7fddb_default.png"},{"username":"marcg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13153301/medium/ebcf60fbf27deb148ec40a659566c6ff_default.png"},{"username":"BeersTeddy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13185230/medium/2f1f4e1effe74a23422b195cbefb2a95_default.png"},{"username":"jamesmcmahon0","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13245578/medium/586aa873b4abddbd9abc6f3de99ab70e.jpeg"},{"username":"b1nar10","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13286752/medium/249b02e3368752cb175446082015737c_default.png"},{"username":"vannCN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13330448/medium/9b8a9ee3611e51e951e22d5fd4eb7d8d.jpg"},{"username":"binswm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13343482/medium/45dde0e6097b9b72705d2eba9dbbc276_default.png"},{"username":"SkewRam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13496556/medium/188f5c2deb7938eda51eb786cc4539ca.jpeg"},{"username":"raphcatarino","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13547726/medium/b003511e67df13a4b4b5689488fa8099.jpg"},{"username":"shillos5","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13600557/medium/b17650b2b84f829d54fa548c44c1ab27.jpeg"},{"username":"wolong98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13641407/medium/f4634edc58c7857a357e5293543c15cf.jpg"},{"username":"spair0039","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14012333/medium/72430e96027c09c19141cac38eae4617.png"},{"username":"MajorMarcin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14072183/medium/0a197e64bb92b6ea94cecbb607512c45.jpg"},{"username":"KosmoMoustache","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14103535/medium/51c438cdd9f86e8811c75f4869de479c.png"},{"username":"Soochaehwa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14118689/medium/496a1ce63111547bf455a1e0a7ac75f1_default.png"},{"username":"garryfield","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14187035/medium/520c964706676045f6eeec1b08edc880.jpg"},{"username":"droidenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14444264/medium/4aa3a8e824f72bc9e5ec0d8de307095e.jpeg"},{"username":"maathis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14489950/medium/6eeddf13a757ee35f78f1763f94d95e1.jpeg"},{"username":"bukvam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14546502/medium/a819dc40a13c57395a17dc46e847d3ce.jpg"},{"username":"gm.cinalli","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14670666/medium/2d466a0fdbda40764526be86c97c0ab4.jpeg"},{"username":"antoine2tt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14722148/medium/f88d926900862dd59007ea4b3419cb9d.png"},{"username":"cretzen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14799754/medium/3ba5c1805ec20c000d5810c07985afe9.jpg"},{"username":"ktKongTong","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14817246/medium/97cfc3c028dbdaf85ebd1102da71e58c.jpeg"},{"username":"marinkaberg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14861042/medium/2d5c4e62613f03082f3e645fa92efd59.jpeg"},{"username":"djismgaming","avatarUrl":"httpsLine truncated
|
||||
@@ -19,7 +19,7 @@
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "16.2.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-config-turbo": "^2.8.20",
|
||||
"eslint-config-turbo": "^2.8.21",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
|
||||
Reference in new issue
Block a user