Compare commits

...
38 Commits
Author SHA1 Message Date
Releases Homarr 6cc1209fc0 chore(release): 1.44.0 [skip ci] 2025-11-21 19:17:06 +00:00
homarr-releases[bot] 2b8fdc0281 chore(release): automatic release v1.44.0 2025-11-21 19:16:09 +00:00
Meier Lukas d9cc35b985 fix(media-requests): incorrect availability mapping (#4520) 2025-11-21 19:05:51 +01:00
Meier Lukas 5a418d73bd fix(iframe): allow clicking links within (#4521) 2025-11-21 19:05:24 +01:00
homarr-renovate[bot] ded4e72e54 fix(deps): update mantine monorepo to ^8.3.8 (#4516)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-20 11:30:16 +00:00
homarr-renovate[bot] de3602da84 chore(deps): update dependency @semantic-release/npm to ^13.1.2 (#4513)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-19 23:30:39 +00:00
homarr-renovate[bot] c80075f1c3 chore(deps): update dependency @types/react to v19.2.5 (#4515)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-19 22:32:13 +00:00
homarr-renovate[bot] be52cb82fd chore(deps): update vitest monorepo to ^4.0.9 (#4509)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-19 21:28:39 +00:00
homarr-renovate[bot] 9304d73054 chore(deps): update dependency hono@&lt;4.6.5 to >=4.10.6 (#4512)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-19 21:27:46 +00:00
homarr-renovate[bot] 912f9ff884 chore(deps): update dependency @types/dockerode to ^3.3.46 (#4514)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-19 20:33:11 +00:00
homarr-renovate[bot] 5e57a4906b fix(deps): update dependency fast-xml-parser to ^5.3.2 (#4510)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-19 09:36:42 +00:00
homarr-renovate[bot] b3db5d5f7b fix(deps): update dependency @tanstack/react-query to ^5.90.9 (#4508)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-19 08:41:31 +00:00
homarr-renovate[bot] ebc5bd170d chore(deps): update dependency glob to v11.1.0 [security] (#4500)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-19 03:19:15 +00:00
homarr-renovate[bot] b4f9fb8b46 fix(deps): update dependency drizzle-kit to ^0.31.7 (#4507)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-18 23:31:02 +00:00
homarr-renovate[bot] 7d0c1a14fd fix(deps): update nextjs monorepo to v16.0.3 (#4504)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-18 22:34:27 +00:00
Meier Lukas c89e35e13c feat(pi-hole): support no-auth (#4480) 2025-11-18 21:05:00 +01:00
homarr-renovate[bot] 8ea6cffb53 fix(deps): update dependency @tanstack/react-query-next-experimental to ^5.91.0 (#4505)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-18 17:39:07 +00:00
homarr-renovate[bot] 1bb96ce5cf chore(deps): update pnpm to v10.22.0 (#4495)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-18 17:38:36 +00:00
homarr-renovate[bot] 6da511bc2e chore(deps): update dependency @types/react to v19.2.4 (#4501)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-18 17:36:32 +00:00
homarr-renovate[bot] 064277373c fix(deps): update dependency next-intl to v4.5.3 (#4503)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-18 17:34:29 +00:00
homarr-crowdin[bot] 88fd0a63b6 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-11-18 00:51:48 +00:00
homarr-renovate[bot] 7fd5bf0699 chore(deps): update dependency jsdom to ^27.2.0 (#4492)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-17 14:35:27 +00:00
homarr-renovate[bot] 1bfa9d7158 chore(deps): update dependency @vitejs/plugin-react to ^5.1.1 (#4493)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-17 13:43:40 +00:00
homarr-renovate[bot] 57140f2c71 chore(deps): update dependency @types/react-dom to v19.2.3 (#4491)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-17 13:43:26 +00:00
homarr-renovate[bot] a5b6203be6 fix(deps): update dependency @tanstack/react-query to ^5.90.8 (#4494)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-17 13:00:35 +00:00
homarr-renovate[bot] aae81d3120 chore(deps): update dependency node to v24.11.1 (#4490)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-17 02:07:01 +00:00
homarr-crowdin[bot] 0fb01c90e9 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-11-17 00:53:59 +00:00
homarr-renovate[bot] 06942ecbcd fix(deps): update dependency next-intl to v4.5.1 (#4484)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-16 16:38:54 +00:00
homarr-renovate[bot] 006dee9e9b chore(deps): update node.js to v24.11.1 (#4481)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-16 16:37:12 +00:00
homarr-renovate[bot] f2df9839f1 chore(deps): update dependency @types/react to v19.2.3 (#4487)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-16 16:36:19 +00:00
homarr-renovate[bot] dcc23e721a chore(deps): update dependency hono@&lt;4.6.5 to >=4.10.5 (#4485)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-16 16:35:49 +00:00
homarr-renovate[bot] 64657d831c fix(deps): update testcontainers-node monorepo to ^11.8.1 (#4486)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-16 15:30:16 +00:00
homarr-renovate[bot] cd124d6e83 fix(deps): update turbo monorepo to ^2.6.1 (#4483)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-16 06:43:04 +00:00
homarr-renovate[bot] 19ecedea43 fix(deps): update dependency sass to ^1.94.0 (#4482)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-16 02:12:03 +00:00
homarr-renovate[bot] 5b55a4751b chore(deps): update dependency rollup@&gt;&#x3d;4.0.0 &lt;4.22.4 to >=4.53.2 (#4479)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-15 09:30:25 +00:00
homarr-renovate[bot] 266b9ffd1b chore(deps): update pnpm to v10.21.0 (#4478)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-15 02:01:31 +00:00
homarr-renovate[bot] 7a3a05b172 chore(deps): update dependency esbuild@&lt;&#x3d;0.24.2 to >=0.27.0 (#4477)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-14 20:36:02 +00:00
homarr-renovate[bot] a62c1385e3 chore(deps): update dependency esbuild to ^0.27.0 (#4476)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-14 19:28:44 +00:00
44 changed files with 1410 additions and 1154 deletions

No files matched your search

@@ -64,7 +64,7 @@ jobs:
- uses: actions/setup-node@v6 - uses: actions/setup-node@v6
if: env.SKIP_RELEASE == 'false' if: env.SKIP_RELEASE == 'false'
with: with:
node-version: 24.11.0 node-version: 24.11.1
cache: "pnpm" cache: "pnpm"
- run: npm i -g pnpm - run: npm i -g pnpm
if: env.SKIP_RELEASE == 'false' if: env.SKIP_RELEASE == 'false'
+1 -1
View File
@@ -1 +1 @@
24.11.0 24.11.1
+23
View File
@@ -1,3 +1,26 @@
## [1.44.0](https://github.com/homarr-labs/homarr/compare/v1.43.3...v1.44.0) (2025-11-21)
### Features
* **pi-hole:** support no-auth ([#4480](https://github.com/homarr-labs/homarr/issues/4480)) ([c89e35e](https://github.com/homarr-labs/homarr/commit/c89e35e13c794bbdf98748c00644b86f7f5dbe68))
### Bug Fixes
* **deps:** update dependency @tanstack/react-query to ^5.90.8 ([#4494](https://github.com/homarr-labs/homarr/issues/4494)) ([a5b6203](https://github.com/homarr-labs/homarr/commit/a5b6203be6ac3c2308614f10ef6a17590917e2c9))
* **deps:** update dependency @tanstack/react-query to ^5.90.9 ([#4508](https://github.com/homarr-labs/homarr/issues/4508)) ([b3db5d5](https://github.com/homarr-labs/homarr/commit/b3db5d5f7bc2d146f0b2c230a52e7bd4efad3bcb))
* **deps:** update dependency @tanstack/react-query-next-experimental to ^5.91.0 ([#4505](https://github.com/homarr-labs/homarr/issues/4505)) ([8ea6cff](https://github.com/homarr-labs/homarr/commit/8ea6cffb537ca54a413c583ab8b8fd24a52185be))
* **deps:** update dependency drizzle-kit to ^0.31.7 ([#4507](https://github.com/homarr-labs/homarr/issues/4507)) ([b4f9fb8](https://github.com/homarr-labs/homarr/commit/b4f9fb8b46a8f74e2d7896dfe98e80618cfbcde4))
* **deps:** update dependency fast-xml-parser to ^5.3.2 ([#4510](https://github.com/homarr-labs/homarr/issues/4510)) ([5e57a49](https://github.com/homarr-labs/homarr/commit/5e57a4906bcedc53d220e983827293f4a699c425))
* **deps:** update dependency next-intl to v4.5.1 ([#4484](https://github.com/homarr-labs/homarr/issues/4484)) ([06942ec](https://github.com/homarr-labs/homarr/commit/06942ecbcd245f5540f2960f437ca2955a5ed0c3))
* **deps:** update dependency next-intl to v4.5.3 ([#4503](https://github.com/homarr-labs/homarr/issues/4503)) ([0642773](https://github.com/homarr-labs/homarr/commit/064277373cacd3309d8da3779d5451d195da7f81))
* **deps:** update dependency sass to ^1.94.0 ([#4482](https://github.com/homarr-labs/homarr/issues/4482)) ([19ecede](https://github.com/homarr-labs/homarr/commit/19ecedea4370924d47545ca8d9d46a0b7c016cc9))
* **deps:** update mantine monorepo to ^8.3.8 ([#4516](https://github.com/homarr-labs/homarr/issues/4516)) ([ded4e72](https://github.com/homarr-labs/homarr/commit/ded4e72e5407b0076495a06111f3b04445f27276))
* **deps:** update nextjs monorepo to v16.0.3 ([#4504](https://github.com/homarr-labs/homarr/issues/4504)) ([7d0c1a1](https://github.com/homarr-labs/homarr/commit/7d0c1a14fdf1731ff44e3d00793cf4cd1c3e9b1f))
* **deps:** update testcontainers-node monorepo to ^11.8.1 ([#4486](https://github.com/homarr-labs/homarr/issues/4486)) ([64657d8](https://github.com/homarr-labs/homarr/commit/64657d831cbe28a59708b280b07889b9f0232743))
* **deps:** update turbo monorepo to ^2.6.1 ([#4483](https://github.com/homarr-labs/homarr/issues/4483)) ([cd124d6](https://github.com/homarr-labs/homarr/commit/cd124d6e8392eef5df0092481ec3cc26599f2902))
* **iframe:** allow clicking links within ([#4521](https://github.com/homarr-labs/homarr/issues/4521)) ([5a418d7](https://github.com/homarr-labs/homarr/commit/5a418d73bdb07cf3d3270983b6881f2de8d58ad7))
* **media-requests:** incorrect availability mapping ([#4520](https://github.com/homarr-labs/homarr/issues/4520)) ([d9cc35b](https://github.com/homarr-labs/homarr/commit/d9cc35b9859a14f44c4e4b13c6518287f8d18e54))
## [1.43.3](https://github.com/homarr-labs/homarr/compare/v1.43.2...v1.43.3) (2025-11-14) ## [1.43.3](https://github.com/homarr-labs/homarr/compare/v1.43.2...v1.43.3) (2025-11-14)
### Bug Fixes ### Bug Fixes
+13 -13
View File
@@ -50,17 +50,17 @@
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0", "@homarr/widgets": "workspace:^0.1.0",
"@mantine/colors-generator": "^8.3.7", "@mantine/colors-generator": "^8.3.8",
"@mantine/core": "^8.3.7", "@mantine/core": "^8.3.8",
"@mantine/dropzone": "^8.3.7", "@mantine/dropzone": "^8.3.8",
"@mantine/hooks": "^8.3.7", "@mantine/hooks": "^8.3.8",
"@mantine/modals": "^8.3.7", "@mantine/modals": "^8.3.8",
"@mantine/tiptap": "^8.3.7", "@mantine/tiptap": "^8.3.8",
"@million/lint": "1.0.14", "@million/lint": "1.0.14",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"@tanstack/react-query": "^5.90.7", "@tanstack/react-query": "^5.90.9",
"@tanstack/react-query-devtools": "^5.90.2", "@tanstack/react-query-devtools": "^5.90.2",
"@tanstack/react-query-next-experimental": "^5.90.2", "@tanstack/react-query-next-experimental": "^5.91.0",
"@trpc/client": "^11.7.1", "@trpc/client": "^11.7.1",
"@trpc/next": "^11.7.1", "@trpc/next": "^11.7.1",
"@trpc/react-query": "^11.7.1", "@trpc/react-query": "^11.7.1",
@@ -78,14 +78,14 @@
"isomorphic-dompurify": "^2.32.0", "isomorphic-dompurify": "^2.32.0",
"jotai": "^2.15.1", "jotai": "^2.15.1",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "16.0.1", "next": "16.0.3",
"postcss-preset-mantine": "^1.18.0", "postcss-preset-mantine": "^1.18.0",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-simple-code-editor": "^0.14.1", "react-simple-code-editor": "^0.14.1",
"sass": "^1.93.3", "sass": "^1.94.0",
"superjson": "2.2.5", "superjson": "2.2.5",
"swagger-ui-react": "^5.30.2", "swagger-ui-react": "^5.30.2",
"use-deep-compare-effect": "^1.8.1", "use-deep-compare-effect": "^1.8.1",
@@ -96,10 +96,10 @@
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.2", "@types/chroma-js": "3.1.2",
"@types/node": "^24.10.0", "@types/node": "^24.10.1",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"@types/react": "19.2.2", "@types/react": "19.2.5",
"@types/react-dom": "19.2.2", "@types/react-dom": "19.2.3",
"@types/swagger-ui-react": "^5.18.0", "@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"eslint": "^9.39.1", "eslint": "^9.39.1",
+2 -2
View File
@@ -47,9 +47,9 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^24.10.0", "@types/node": "^24.10.1",
"dotenv-cli": "^11.0.0", "dotenv-cli": "^11.0.0",
"esbuild": "^0.26.0", "esbuild": "^0.27.0",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"tsx": "4.20.4", "tsx": "4.20.4",
+1 -1
View File
@@ -34,7 +34,7 @@
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"esbuild": "^0.26.0", "esbuild": "^0.27.0",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"typescript": "^5.9.3" "typescript": "^5.9.3"
+16 -16
View File
@@ -1,6 +1,6 @@
{ {
"name": "homarr", "name": "homarr",
"version": "1.43.3", "version": "1.44.0",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "cross-env CI=true turbo build", "build": "cross-env CI=true turbo build",
@@ -40,27 +40,27 @@
"@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^12.0.2", "@semantic-release/github": "^12.0.2",
"@semantic-release/npm": "^13.1.1", "@semantic-release/npm": "^13.1.2",
"@semantic-release/release-notes-generator": "^14.1.0", "@semantic-release/release-notes-generator": "^14.1.0",
"@testcontainers/redis": "^11.8.0", "@testcontainers/redis": "^11.8.1",
"@turbo/gen": "^2.6.0", "@turbo/gen": "^2.6.1",
"@vitejs/plugin-react": "^5.1.0", "@vitejs/plugin-react": "^5.1.1",
"@vitest/coverage-v8": "^4.0.8", "@vitest/coverage-v8": "^4.0.9",
"@vitest/ui": "^4.0.8", "@vitest/ui": "^4.0.9",
"conventional-changelog-conventionalcommits": "^9.1.0", "conventional-changelog-conventionalcommits": "^9.1.0",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"jsdom": "^27.1.0", "jsdom": "^27.2.0",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"semantic-release": "^25.0.2", "semantic-release": "^25.0.2",
"testcontainers": "^11.8.0", "testcontainers": "^11.8.1",
"turbo": "^2.6.0", "turbo": "^2.6.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^4.0.8" "vitest": "^4.0.9"
}, },
"packageManager": "pnpm@10.20.0", "packageManager": "pnpm@10.22.0",
"engines": { "engines": {
"node": ">=24.11.0" "node": ">=24.11.1"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
@@ -80,15 +80,15 @@
"axios@>=1.0.0 <1.8.2": ">=1.13.2", "axios@>=1.0.0 <1.8.2": ">=1.13.2",
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1", "brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1", "brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
"esbuild@<=0.24.2": ">=0.26.0", "esbuild@<=0.24.2": ">=0.27.0",
"form-data@>=4.0.0 <4.0.4": ">=4.0.4", "form-data@>=4.0.0 <4.0.4": ">=4.0.4",
"hono@<4.6.5": ">=4.10.4", "hono@<4.6.5": ">=4.10.6",
"linkifyjs@<4.3.2": ">=4.3.2", "linkifyjs@<4.3.2": ">=4.3.2",
"nanoid@>=4.0.0 <5.0.9": ">=5.1.6", "nanoid@>=4.0.0 <5.0.9": ">=5.1.6",
"prismjs@<1.30.0": ">=1.30.0", "prismjs@<1.30.0": ">=1.30.0",
"proxmox-api>undici": "7.16.0", "proxmox-api>undici": "7.16.0",
"react-is": "^19.2.0", "react-is": "^19.2.0",
"rollup@>=4.0.0 <4.22.4": ">=4.53.1", "rollup@>=4.0.0 <4.22.4": ">=4.53.2",
"sha.js@<=2.4.11": ">=2.4.12", "sha.js@<=2.4.11": ">=2.4.12",
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1", "tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.1", "tar-fs@>=2.0.0 <2.1.3": ">=3.1.1",
+2 -2
View File
@@ -43,13 +43,13 @@
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.4.0", "@kubernetes/client-node": "^1.4.0",
"@tanstack/react-query": "^5.90.7", "@tanstack/react-query": "^5.90.9",
"@trpc/client": "^11.7.1", "@trpc/client": "^11.7.1",
"@trpc/react-query": "^11.7.1", "@trpc/react-query": "^11.7.1",
"@trpc/server": "^11.7.1", "@trpc/server": "^11.7.1",
"@trpc/tanstack-react-query": "^11.7.1", "@trpc/tanstack-react-query": "^11.7.1",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"next": "16.0.1", "next": "16.0.3",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"superjson": "2.2.5", "superjson": "2.2.5",
@@ -3,6 +3,7 @@ import { z } from "zod/v4";
import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations";
import { mediaRequestStatusConfiguration } from "@homarr/integrations/types";
import type { MediaRequest } from "@homarr/integrations/types"; import type { MediaRequest } from "@homarr/integrations/types";
import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list"; import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list";
import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats"; import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats";
@@ -35,7 +36,10 @@ export const mediaRequestsRouter = createTRPCRouter({
return dataB.createdAt.getTime() - dataA.createdAt.getTime(); return dataB.createdAt.getTime() - dataA.createdAt.getTime();
} }
return dataA.status - dataB.status; return (
mediaRequestStatusConfiguration[dataA.status].position -
mediaRequestStatusConfiguration[dataB.status].position
);
}); });
}), }),
subscribeToLatestRequests: publicProcedure subscribeToLatestRequests: publicProcedure
+1 -1
View File
@@ -35,7 +35,7 @@
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"cookies": "^0.9.1", "cookies": "^0.9.1",
"ldapts": "8.0.9", "ldapts": "8.0.9",
"next": "16.0.1", "next": "16.0.3",
"next-auth": "5.0.0-beta.30", "next-auth": "5.0.0-beta.30",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
+1 -1
View File
@@ -34,7 +34,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"esbuild": "^0.26.0", "esbuild": "^0.27.0",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
+1 -1
View File
@@ -32,7 +32,7 @@
"@paralleldrive/cuid2": "^3.1.0", "@paralleldrive/cuid2": "^3.1.0",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"dns-caching": "^0.2.7", "dns-caching": "^0.2.7",
"next": "16.0.1", "next": "16.0.3",
"octokit": "^5.0.5", "octokit": "^5.0.5",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
+2 -2
View File
@@ -29,7 +29,7 @@
"@homarr/core": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0", "@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"@tanstack/react-query": "^5.90.7", "@tanstack/react-query": "^5.90.9",
"@trpc/client": "^11.7.1", "@trpc/client": "^11.7.1",
"@trpc/server": "^11.7.1", "@trpc/server": "^11.7.1",
"@trpc/tanstack-react-query": "^11.7.1", "@trpc/tanstack-react-query": "^11.7.1",
@@ -42,7 +42,7 @@
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"@types/react": "19.2.2", "@types/react": "19.2.5",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
+5 -5
View File
@@ -49,13 +49,13 @@
"@homarr/definitions": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^8.3.7", "@mantine/core": "^8.3.8",
"@paralleldrive/cuid2": "^3.1.0", "@paralleldrive/cuid2": "^3.1.0",
"@testcontainers/mysql": "^11.8.0", "@testcontainers/mysql": "^11.8.1",
"@testcontainers/postgresql": "^11.8.0", "@testcontainers/postgresql": "^11.8.1",
"better-sqlite3": "^12.4.1", "better-sqlite3": "^12.4.1",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"drizzle-kit": "^0.31.6", "drizzle-kit": "^0.31.7",
"drizzle-orm": "^0.44.7", "drizzle-orm": "^0.44.7",
"drizzle-zod": "^0.8.3", "drizzle-zod": "^0.8.3",
"mysql2": "3.15.3", "mysql2": "3.15.3",
@@ -69,7 +69,7 @@
"@types/better-sqlite3": "7.6.13", "@types/better-sqlite3": "7.6.13",
"@types/pg": "^8.15.6", "@types/pg": "^8.15.6",
"dotenv-cli": "^11.0.0", "dotenv-cli": "^11.0.0",
"esbuild": "^0.26.0", "esbuild": "^0.27.0",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"tsx": "4.20.4", "tsx": "4.20.4",
+1 -1
View File
@@ -24,7 +24,7 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"fast-xml-parser": "^5.3.1", "fast-xml-parser": "^5.3.2",
"zod": "^4.1.12" "zod": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {
+1 -1
View File
@@ -145,7 +145,7 @@ export const integrationDefs = {
}, },
piHole: { piHole: {
name: "Pi-hole", name: "Pi-hole",
secretKinds: [["apiKey"]], secretKinds: [["apiKey"], []],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/pi-hole.svg", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/pi-hole.svg",
category: ["dnsHole"], category: ["dnsHole"],
documentationUrl: createDocumentationLink("/docs/integrations/pi-hole"), documentationUrl: createDocumentationLink("/docs/integrations/pi-hole"),
+1 -1
View File
@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "^3.3.45", "@types/dockerode": "^3.3.46",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
+1 -1
View File
@@ -26,7 +26,7 @@
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/form": "^8.3.7", "@mantine/form": "^8.3.8",
"mantine-form-zod-resolver": "^1.3.0", "mantine-form-zod-resolver": "^1.3.0",
"zod": "^4.1.12" "zod": "^4.1.12"
}, },
+1 -1
View File
@@ -30,7 +30,7 @@
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.3.7", "@mantine/core": "^8.3.8",
"react": "19.2.0", "react": "19.2.0",
"zod": "^4.1.12" "zod": "^4.1.12"
}, },
+1 -1
View File
@@ -38,7 +38,7 @@ export type {
FirewallMemorySummary, FirewallMemorySummary,
} from "./interfaces/firewall-summary/firewall-summary-types"; } from "./interfaces/firewall-summary/firewall-summary-types";
export type { SystemHealthMonitoring } from "./interfaces/health-monitoring/health-monitoring-types"; export type { SystemHealthMonitoring } from "./interfaces/health-monitoring/health-monitoring-types";
export { MediaRequestStatus } from "./interfaces/media-requests/media-request-types"; export { UpstreamMediaRequestStatus } from "./interfaces/media-requests/media-request-types";
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request-types"; export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request-types";
export type { StreamSession } from "./interfaces/media-server/media-server-types"; export type { StreamSession } from "./interfaces/media-server/media-server-types";
export type { export type {
@@ -1,3 +1,5 @@
import { objectKeys } from "@homarr/common";
interface SerieSeason { interface SerieSeason {
id: number; id: number;
seasonNumber: number; seasonNumber: number;
@@ -34,6 +36,64 @@ export interface MediaRequest {
requestedBy?: Omit<RequestUser, "requestCount">; requestedBy?: Omit<RequestUser, "requestCount">;
} }
export const mediaAvailabilityConfiguration = {
available: {
color: "green",
},
partiallyAvailable: {
color: "yellow",
},
processing: {
color: "blue",
},
requested: {
color: "violet",
},
pending: {
color: "violet",
},
unknown: {
color: "orange",
},
deleted: {
color: "red",
},
blacklisted: {
color: "gray",
},
} satisfies Record<string, { color: string }>;
export const mediaAvailabilities = objectKeys(mediaAvailabilityConfiguration);
export type MediaAvailability = (typeof mediaAvailabilities)[number];
export const mediaRequestStatusConfiguration = {
pending: {
color: "blue",
position: 1,
},
approved: {
color: "green",
position: 2,
},
declined: {
color: "red",
position: 3,
},
failed: {
color: "red",
position: 4,
},
completed: {
color: "green",
position: 5,
},
} satisfies Record<string, { color: string; position: number }>;
export const mediaRequestStatuses = objectKeys(mediaRequestStatusConfiguration);
export type MediaRequestStatus = (typeof mediaRequestStatuses)[number];
export interface MediaRequestList { export interface MediaRequestList {
integration: { integration: {
id: string; id: string;
@@ -66,7 +126,7 @@ export interface MediaRequestStats {
} }
// https://github.com/fallenbagel/jellyseerr/blob/0fd03f38480f853e7015ad9229ed98160e37602e/server/constants/media.ts#L1 // https://github.com/fallenbagel/jellyseerr/blob/0fd03f38480f853e7015ad9229ed98160e37602e/server/constants/media.ts#L1
export enum MediaRequestStatus { export enum UpstreamMediaRequestStatus {
PendingApproval = 1, PendingApproval = 1,
Approved = 2, Approved = 2,
Declined = 3, Declined = 3,
@@ -75,12 +135,12 @@ export enum MediaRequestStatus {
} }
// https://github.com/fallenbagel/jellyseerr/blob/0fd03f38480f853e7015ad9229ed98160e37602e/server/constants/media.ts#L14 // https://github.com/fallenbagel/jellyseerr/blob/0fd03f38480f853e7015ad9229ed98160e37602e/server/constants/media.ts#L14
export enum MediaAvailability { export enum UpstreamMediaAvailability {
Unknown = 1, Unknown = 1,
Pending = 2, Pending = 2,
Processing = 3, Processing = 3,
PartiallyAvailable = 4, PartiallyAvailable = 4,
Available = 5, Available = 5,
Blacklisted = 6, JellyseerrBlacklistedOrOverseerrDeleted = 6,
Deleted = 7, JellyseerrDeleted = 7,
} }
@@ -1,3 +1,12 @@
import type { MediaAvailability } from "../interfaces/media-requests/media-request-types";
import { UpstreamMediaAvailability } from "../interfaces/media-requests/media-request-types";
import { OverseerrIntegration } from "../overseerr/overseerr-integration"; import { OverseerrIntegration } from "../overseerr/overseerr-integration";
export class JellyseerrIntegration extends OverseerrIntegration {} export class JellyseerrIntegration extends OverseerrIntegration {
protected override mapAvailability(availability: UpstreamMediaAvailability, inProgress: boolean): MediaAvailability {
// Availability statuses are not exactly the same between Jellyseerr and Overseerr (Jellyseerr has "blacklisted" additionally (deleted is the same value in overseerr))
if (availability === UpstreamMediaAvailability.JellyseerrBlacklistedOrOverseerrDeleted) return "blacklisted";
if (availability === UpstreamMediaAvailability.JellyseerrDeleted) return "deleted";
return super.mapAvailability(availability, inProgress);
}
}
@@ -1,8 +1,13 @@
import { objectEntries } from "@homarr/common";
import type { IMediaRequestIntegration } from "../../interfaces/media-requests/media-request-integration"; import type { IMediaRequestIntegration } from "../../interfaces/media-requests/media-request-integration";
import type { MediaInformation, MediaRequest, RequestStats, RequestUser } from "../../types"; import type {
import { MediaAvailability, MediaRequestStatus } from "../../types"; MediaAvailability,
MediaInformation,
MediaRequest,
MediaRequestStatus,
RequestStats,
RequestUser,
} from "../../types";
import { mediaAvailabilities, mediaRequestStatuses } from "../../types";
export class MediaRequestMockService implements IMediaRequestIntegration { export class MediaRequestMockService implements IMediaRequestIntegration {
public async getSeriesInformationAsync(mediaType: "movie" | "tv", id: number): Promise<MediaInformation> { public async getSeriesInformationAsync(mediaType: "movie" | "tv", id: number): Promise<MediaInformation> {
@@ -86,12 +91,10 @@ export class MediaRequestMockService implements IMediaRequestIntegration {
} }
private static randomAvailability(): MediaAvailability { private static randomAvailability(): MediaAvailability {
const values = objectEntries(MediaAvailability).filter(([key]) => typeof key === "number"); return mediaAvailabilities.at(Math.floor(Math.random() * mediaAvailabilities.length)) ?? "unknown";
return values[Math.floor(Math.random() * values.length)]?.[1] ?? MediaAvailability.Available;
} }
private static randomStatus(): MediaRequestStatus { private static randomStatus(): MediaRequestStatus {
const values = objectEntries(MediaRequestStatus).filter(([key]) => typeof key === "number"); return mediaRequestStatuses.at(Math.floor(Math.random() * mediaRequestStatuses.length)) ?? "pending";
return values[Math.floor(Math.random() * values.length)]?.[1] ?? MediaRequestStatus.PendingApproval;
} }
} }
@@ -9,8 +9,17 @@ import type { ISearchableIntegration } from "../base/searchable-integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error"; import { TestConnectionError } from "../base/test-connection/test-connection-error";
import type { TestingResult } from "../base/test-connection/test-connection-service"; import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { IMediaRequestIntegration } from "../interfaces/media-requests/media-request-integration"; import type { IMediaRequestIntegration } from "../interfaces/media-requests/media-request-integration";
import type { MediaRequest, RequestStats, RequestUser } from "../interfaces/media-requests/media-request-types"; import type {
import { MediaAvailability, MediaRequestStatus } from "../interfaces/media-requests/media-request-types"; MediaAvailability,
MediaRequest,
MediaRequestStatus,
RequestStats,
RequestUser,
} from "../interfaces/media-requests/media-request-types";
import {
UpstreamMediaAvailability,
UpstreamMediaRequestStatus,
} from "../interfaces/media-requests/media-request-types";
interface OverseerrSearchResult { interface OverseerrSearchResult {
id: number; id: number;
@@ -128,7 +137,7 @@ export class OverseerrIntegration
if (pendingResults.length > 0 && allResults.length > 0) { if (pendingResults.length > 0 && allResults.length > 0) {
requests = pendingResults.concat( requests = pendingResults.concat(
allResults.filter(({ status }) => status !== MediaRequestStatus.PendingApproval), allResults.filter(({ status }) => status !== UpstreamMediaRequestStatus.PendingApproval),
); );
} else if (pendingResults.length > 0) requests = pendingResults; } else if (pendingResults.length > 0) requests = pendingResults;
else if (allResults.length > 0) requests = allResults; else if (allResults.length > 0) requests = allResults;
@@ -137,11 +146,15 @@ export class OverseerrIntegration
return await Promise.all( return await Promise.all(
requests.map(async (request): Promise<MediaRequest> => { requests.map(async (request): Promise<MediaRequest> => {
const information = await this.getItemInformationAsync(request.media.tmdbId, request.type); const information = await this.getItemInformationAsync(request.media.tmdbId, request.type);
// See https://github.com/seerr-team/seerr/blob/af083a3cd5c3e3d5d7917fdf4fdd67fe3f39c46b/src/components/StatusBadge/index.tsx#L40
const inProgress = (request.media.downloadStatus ?? []).length >= 1;
return { return {
id: request.id, id: request.id,
name: information.name, name: information.name,
status: request.status, status: this.mapRequestStatus(request.status),
availability: request.media.status, availability: this.mapAvailability(request.media.status, inProgress),
backdropImageUrl: `https://image.tmdb.org/t/p/original/${information.backdropPath}`, backdropImageUrl: `https://image.tmdb.org/t/p/original/${information.backdropPath}`,
posterImagePath: `https://image.tmdb.org/t/p/w600_and_h900_bestv2/${information.posterPath}`, posterImagePath: `https://image.tmdb.org/t/p/w600_and_h900_bestv2/${information.posterPath}`,
href: this.externalUrl(`/${request.type}/${request.media.tmdbId}`).toString(), href: this.externalUrl(`/${request.type}/${request.media.tmdbId}`).toString(),
@@ -161,6 +174,42 @@ export class OverseerrIntegration
); );
} }
protected mapRequestStatus(status: UpstreamMediaRequestStatus): MediaRequestStatus {
switch (status) {
case UpstreamMediaRequestStatus.PendingApproval:
return "pending";
case UpstreamMediaRequestStatus.Approved:
return "approved";
case UpstreamMediaRequestStatus.Declined:
return "declined";
case UpstreamMediaRequestStatus.Failed:
return "failed";
case UpstreamMediaRequestStatus.Completed:
return "completed";
default:
return "failed";
}
}
// See https://github.com/seerr-team/seerr/blob/af083a3cd5c3e3d5d7917fdf4fdd67fe3f39c46b/src/components/StatusBadge/index.tsx#L153-L387
protected mapAvailability(availability: UpstreamMediaAvailability, inProgress: boolean): MediaAvailability {
switch (availability) {
case UpstreamMediaAvailability.Available:
return inProgress ? "processing" : "available";
case UpstreamMediaAvailability.PartiallyAvailable:
return inProgress ? "processing" : "partiallyAvailable";
case UpstreamMediaAvailability.Processing:
return inProgress ? "processing" : "requested";
case UpstreamMediaAvailability.Pending:
return "pending";
case UpstreamMediaAvailability.JellyseerrBlacklistedOrOverseerrDeleted:
return "deleted";
case UpstreamMediaAvailability.Unknown:
default:
return inProgress ? "processing" : "unknown";
}
}
public async getStatsAsync(): Promise<RequestStats> { public async getStatsAsync(): Promise<RequestStats> {
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/request/count"), { const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/request/count"), {
headers: { headers: {
@@ -339,11 +388,12 @@ const getRequestsSchema = z.object({
.array( .array(
z.object({ z.object({
id: z.number(), id: z.number(),
status: z.nativeEnum(MediaRequestStatus), status: z.enum(UpstreamMediaRequestStatus),
createdAt: z.string().transform((value) => new Date(value)), createdAt: z.string().transform((value) => new Date(value)),
media: z.object({ media: z.object({
status: z.nativeEnum(MediaAvailability), status: z.enum(UpstreamMediaAvailability),
tmdbId: z.number(), tmdbId: z.number(),
downloadStatus: z.array(z.unknown()).optional(),
}), }),
type: z.enum(["movie", "tv"]), type: z.enum(["movie", "tv"]),
requestedBy: z requestedBy: z
@@ -17,7 +17,7 @@ import { dnsBlockingGetSchema, sessionResponseSchema, statsSummaryGetSchema } fr
const localLogger = logger.child({ module: "PiHoleIntegrationV6" }); const localLogger = logger.child({ module: "PiHoleIntegrationV6" });
export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIntegration { export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIntegration {
private readonly sessionStore: SessionStore<string>; private readonly sessionStore: SessionStore<{ sid: string | null }>;
constructor(integration: IntegrationInput) { constructor(integration: IntegrationInput) {
super(integration); super(integration);
@@ -28,7 +28,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
const response = await this.withAuthAsync(async (sessionId) => { const response = await this.withAuthAsync(async (sessionId) => {
return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), { return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), {
headers: { headers: {
sid: sessionId, sid: sessionId ?? undefined,
}, },
}); });
}); });
@@ -46,7 +46,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
const response = await this.withAuthAsync(async (sessionId) => { const response = await this.withAuthAsync(async (sessionId) => {
return fetchWithTrustedCertificatesAsync(this.url("/api/stats/summary"), { return fetchWithTrustedCertificatesAsync(this.url("/api/stats/summary"), {
headers: { headers: {
sid: sessionId, sid: sessionId ?? undefined,
}, },
}); });
}); });
@@ -85,7 +85,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
const response = await this.withAuthAsync(async (sessionId) => { const response = await this.withAuthAsync(async (sessionId) => {
return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), { return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), {
headers: { headers: {
sid: sessionId, sid: sessionId ?? undefined,
}, },
body: JSON.stringify({ blocking: true }), body: JSON.stringify({ blocking: true }),
method: "POST", method: "POST",
@@ -101,7 +101,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
const response = await this.withAuthAsync(async (sessionId) => { const response = await this.withAuthAsync(async (sessionId) => {
return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), { return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), {
headers: { headers: {
sid: sessionId, sid: sessionId ?? undefined,
}, },
body: JSON.stringify({ blocking: false, timer: duration }), body: JSON.stringify({ blocking: false, timer: duration }),
method: "POST", method: "POST",
@@ -118,12 +118,16 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
* @param callback * @param callback
* @returns * @returns
*/ */
private async withAuthAsync(callback: (sessionId: string) => Promise<UndiciResponse>) { private async withAuthAsync(callback: (sessionId: string | null) => Promise<UndiciResponse>) {
if (!super.hasSecretValue("apiKey")) {
return await callback(null);
}
const storedSession = await this.sessionStore.getAsync(); const storedSession = await this.sessionStore.getAsync();
if (storedSession) { if (storedSession) {
localLogger.debug("Using stored session for request", { integrationId: this.integration.id }); localLogger.debug("Using stored session for request", { integrationId: this.integration.id });
const response = await callback(storedSession); const response = await callback(storedSession.sid);
if (response.status !== 401) { if (response.status !== 401) {
return response; return response;
} }
@@ -132,7 +136,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
} }
const sessionId = await this.getSessionAsync(); const sessionId = await this.getSessionAsync();
await this.sessionStore.setAsync(sessionId); await this.sessionStore.setAsync({ sid: sessionId });
const response = await callback(sessionId); const response = await callback(sessionId);
return response; return response;
} }
@@ -141,11 +145,13 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
* Get a session id from the Pi-hole server * Get a session id from the Pi-hole server
* @returns The session id * @returns The session id
*/ */
private async getSessionAsync(fetchAsync: typeof undiciFetch = fetchWithTrustedCertificatesAsync): Promise<string> { private async getSessionAsync(
const apiKey = super.getSecretValue("apiKey"); fetchAsync: typeof undiciFetch = fetchWithTrustedCertificatesAsync,
): Promise<string | null> {
const apiKey = super.hasSecretValue("apiKey") ? super.getSecretValue("apiKey") : null;
const response = await fetchAsync(this.url("/api/auth"), { const response = await fetchAsync(this.url("/api/auth"), {
method: "POST", method: "POST",
body: JSON.stringify({ password: apiKey }), body: JSON.stringify({ password: apiKey ?? "" }),
headers: { headers: {
"User-Agent": "Homarr", "User-Agent": "Homarr",
}, },
@@ -156,8 +162,13 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
const data = await response.json(); const data = await response.json();
const result = await sessionResponseSchema.parseAsync(data); const result = await sessionResponseSchema.parseAsync(data);
if (!result.session.sid) { if (!result.session.valid) {
throw new ResponseError({ status: 401, url: response.url }); throw new ResponseError(
{ status: 401, url: response.url },
{
cause: result.session.message ? new Error(result.session.message) : undefined,
},
);
} }
localLogger.info("Received session id successfully", { integrationId: this.integration.id }); localLogger.info("Received session id successfully", { integrationId: this.integration.id });
@@ -170,9 +181,14 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
* @param sessionId The session id to remove * @param sessionId The session id to remove
*/ */
private async clearSessionAsync( private async clearSessionAsync(
sessionId: string, sessionId: string | null,
fetchAsync: typeof undiciFetch = fetchWithTrustedCertificatesAsync, fetchAsync: typeof undiciFetch = fetchWithTrustedCertificatesAsync,
) { ) {
if (!sessionId) {
localLogger.debug("No session id to clear");
return;
}
const response = await fetchAsync(this.url("/api/auth"), { const response = await fetchAsync(this.url("/api/auth"), {
method: "DELETE", method: "DELETE",
headers: { headers: {
@@ -2,6 +2,7 @@ import { z } from "zod/v4";
export const sessionResponseSchema = z.object({ export const sessionResponseSchema = z.object({
session: z.object({ session: z.object({
valid: z.boolean(),
sid: z.string().nullable(), sid: z.string().nullable(),
message: z.string().nullable(), message: z.string().nullable(),
}), }),
+2 -2
View File
@@ -33,10 +33,10 @@
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.3.7", "@mantine/core": "^8.3.8",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"next": "16.0.1", "next": "16.0.3",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"zod": "^4.1.12" "zod": "^4.1.12"
+2 -2
View File
@@ -24,8 +24,8 @@
"dependencies": { "dependencies": {
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^8.3.7", "@mantine/core": "^8.3.8",
"@mantine/hooks": "^8.3.7", "@mantine/hooks": "^8.3.8",
"react": "19.2.0" "react": "19.2.0"
}, },
"devDependencies": { "devDependencies": {
+1 -1
View File
@@ -24,7 +24,7 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@mantine/notifications": "^8.3.7", "@mantine/notifications": "^8.3.8",
"@tabler/icons-react": "^3.35.0" "@tabler/icons-react": "^3.35.0"
}, },
"devDependencies": { "devDependencies": {
+3 -3
View File
@@ -37,10 +37,10 @@
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.3.7", "@mantine/core": "^8.3.8",
"@mantine/hooks": "^8.3.7", "@mantine/hooks": "^8.3.8",
"adm-zip": "0.5.16", "adm-zip": "0.5.16",
"next": "16.0.1", "next": "16.0.3",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"superjson": "2.2.5", "superjson": "2.2.5",
+2 -2
View File
@@ -26,8 +26,8 @@
"@homarr/api": "workspace:^0.1.0", "@homarr/api": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@mantine/dates": "^8.3.7", "@mantine/dates": "^8.3.8",
"next": "16.0.1", "next": "16.0.3",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0" "react-dom": "19.2.0"
}, },
+4 -4
View File
@@ -33,12 +33,12 @@
"@homarr/settings": "workspace:^0.1.0", "@homarr/settings": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^8.3.7", "@mantine/core": "^8.3.8",
"@mantine/hooks": "^8.3.7", "@mantine/hooks": "^8.3.8",
"@mantine/spotlight": "^8.3.7", "@mantine/spotlight": "^8.3.8",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"jotai": "^2.15.1", "jotai": "^2.15.1",
"next": "16.0.1", "next": "16.0.3",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"use-deep-compare-effect": "^1.8.1" "use-deep-compare-effect": "^1.8.1"
+2 -2
View File
@@ -32,8 +32,8 @@
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"deepmerge": "4.3.1", "deepmerge": "4.3.1",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "16.0.1", "next": "16.0.3",
"next-intl": "4.5.0", "next-intl": "4.5.3",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0" "react-dom": "19.2.0"
}, },
+10 -10
View File
@@ -649,14 +649,14 @@
"app": { "app": {
"option": { "option": {
"existing": { "existing": {
"title": "", "title": "现有",
"label": "" "label": "选择现有应用"
}, },
"new": { "new": {
"title": "", "title": "新建",
"url": { "url": {
"label": "", "label": "应用网址",
"description": "" "description": "当从面板访问时应用将打开的网址"
} }
} }
} }
@@ -676,9 +676,9 @@
}, },
"app": { "app": {
"action": { "action": {
"add": "", "add": "链接应用",
"remove": "", "remove": "取消连接",
"select": "" "select": "选择要链接的应用"
} }
} }
}, },
@@ -709,7 +709,7 @@
"description": "集成“{kind}”可以与搜索引擎一起使用。勾选此项可自动配置搜索引擎。" "description": "集成“{kind}”可以与搜索引擎一起使用。勾选此项可自动配置搜索引擎。"
}, },
"app": { "app": {
"sectionTitle": "" "sectionTitle": "关联的应用"
}, },
"createApp": { "createApp": {
"label": "创建应用", "label": "创建应用",
@@ -1051,7 +1051,7 @@
"add": "添加", "add": "添加",
"apply": "应用", "apply": "应用",
"backToOverview": "返回概览", "backToOverview": "返回概览",
"change": "", "change": "更改",
"create": "创建", "create": "创建",
"createAnother": "创建并重新开始", "createAnother": "创建并重新开始",
"edit": "编辑", "edit": "编辑",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Unknown", "unknown": "Unknown",
"pending": "Pending", "pending": "Pending",
"processing": "Processing", "processing": "Processing",
"requested": "Requested",
"partiallyAvailable": "Partial", "partiallyAvailable": "Partial",
"available": "Available", "available": "Available",
"blacklisted": "Blacklisted", "blacklisted": "Blacklisted",
+1 -1
View File
@@ -3748,7 +3748,7 @@
"label": "Nome" "label": "Nome"
}, },
"namespace": { "namespace": {
"label": "" "label": "Spazio dei nomi"
}, },
"className": { "className": {
"label": "Nome classe" "label": "Nome classe"
+21 -21
View File
@@ -649,14 +649,14 @@
"app": { "app": {
"option": { "option": {
"existing": { "existing": {
"title": "", "title": "既存",
"label": "" "label": "既存のアプリを選択"
}, },
"new": { "new": {
"title": "", "title": "新規作成",
"url": { "url": {
"label": "", "label": "アプリURL",
"description": "" "description": "ダッシュボードからアクセスしたときにアプリが開くURL"
} }
} }
} }
@@ -676,9 +676,9 @@
}, },
"app": { "app": {
"action": { "action": {
"add": "", "add": "アプリにリンクする",
"remove": "", "remove": "リンクを解除する",
"select": "" "select": "リンクするアプリを選択する"
} }
} }
}, },
@@ -709,7 +709,7 @@
"description": "連携機能 \"{kind}\" は、検索エンジンで使用できます。検索エンジンを自動的に設定するには、これにチェックを入れてください。" "description": "連携機能 \"{kind}\" は、検索エンジンで使用できます。検索エンジンを自動的に設定するには、これにチェックを入れてください。"
}, },
"app": { "app": {
"sectionTitle": "" "sectionTitle": "リンクされたアプリ"
}, },
"createApp": { "createApp": {
"label": "アプリの作成", "label": "アプリの作成",
@@ -1051,7 +1051,7 @@
"add": "追加", "add": "追加",
"apply": "適用", "apply": "適用",
"backToOverview": "概要に戻る", "backToOverview": "概要に戻る",
"change": "", "change": "変更する",
"create": "作成", "create": "作成",
"createAnother": "作成・新規入力", "createAnother": "作成・新規入力",
"edit": "編集", "edit": "編集",
@@ -1174,8 +1174,8 @@
}, },
"unit": { "unit": {
"speed": { "speed": {
"kilometersPerHour": "", "kilometersPerHour": "km/h",
"milesPerHour": "" "milesPerHour": "mph"
} }
} }
}, },
@@ -1190,7 +1190,7 @@
"label": "タイトル" "label": "タイトル"
}, },
"customCssClasses": { "customCssClasses": {
"label": "" "label": "カスタム css クラス"
}, },
"borderColor": { "borderColor": {
"label": "境界線の色" "label": "境界線の色"
@@ -1780,7 +1780,7 @@
"description": "現在の天気のみ表示" "description": "現在の天気のみ表示"
}, },
"useImperialSpeed": { "useImperialSpeed": {
"label": "" "label": "風速にmphを使用する"
}, },
"location": { "location": {
"label": "天候の場所" "label": "天候の場所"
@@ -1800,12 +1800,12 @@
"description": "日付がどのように見えるか" "description": "日付がどのように見えるか"
} }
}, },
"currentWindSpeed": "時速 {currentWindSpeed} km", "currentWindSpeed": "時速 {currentWindSpeed} {unit}",
"dailyForecast": { "dailyForecast": {
"sunrise": "日出", "sunrise": "日出",
"sunset": "日没", "sunset": "日没",
"maxWindSpeed": "最大風速: {maxWindSpeed} km/h", "maxWindSpeed": "最大風速: {maxWindSpeed} {unit}",
"maxWindGusts": "最大瞬間風速: {maxWindGusts} km/h" "maxWindGusts": "最大瞬間風速: {maxWindGusts} {unit}"
}, },
"kind": { "kind": {
"clear": "晴れ", "clear": "晴れ",
@@ -3013,8 +3013,8 @@
"integration": "連携機能", "integration": "連携機能",
"app": "アプリ", "app": "アプリ",
"group": "グループ", "group": "グループ",
"searchEngine": "", "searchEngine": "検索エンジン",
"media": "" "media": "メディア"
}, },
"statisticLabel": { "statisticLabel": {
"boards": "ボード", "boards": "ボード",
@@ -3023,8 +3023,8 @@
"authorization": "認可" "authorization": "認可"
}, },
"heroBanner": { "heroBanner": {
"title": "", "title": "おかえりなさい",
"subtitle": "" "subtitle": "{app} ボード"
} }
}, },
"board": { "board": {
+4 -4
View File
@@ -30,12 +30,12 @@
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.3.7", "@mantine/core": "^8.3.8",
"@mantine/dates": "^8.3.7", "@mantine/dates": "^8.3.8",
"@mantine/hooks": "^8.3.7", "@mantine/hooks": "^8.3.8",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "16.0.1", "next": "16.0.3",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"svgson": "^5.3.1" "svgson": "^5.3.1"
+4 -4
View File
@@ -48,9 +48,9 @@
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/charts": "^8.3.7", "@mantine/charts": "^8.3.8",
"@mantine/core": "^8.3.7", "@mantine/core": "^8.3.8",
"@mantine/hooks": "^8.3.7", "@mantine/hooks": "^8.3.8",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"@tiptap/extension-color": "2.27.1", "@tiptap/extension-color": "2.27.1",
"@tiptap/extension-highlight": "2.27.1", "@tiptap/extension-highlight": "2.27.1",
@@ -72,7 +72,7 @@
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"mantine-form-zod-resolver": "^1.3.0", "mantine-form-zod-resolver": "^1.3.0",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "16.0.1", "next": "16.0.3",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
+7 -1
View File
@@ -85,7 +85,13 @@ const getAllowedPermissions = (
const getSandboxFlags = ( const getSandboxFlags = (
permissions: Omit<WidgetComponentProps<"iframe">["options"], "embedUrl" | "allowScrolling">, permissions: Omit<WidgetComponentProps<"iframe">["options"], "embedUrl" | "allowScrolling">,
) => { ) => {
const baseSandbox = ["allow-scripts", "allow-same-origin", "allow-forms", "allow-popups"]; const baseSandbox = [
"allow-scripts",
"allow-same-origin",
"allow-forms",
"allow-popups",
"allow-top-navigation-by-user-activation",
];
if (permissions.allowFullScreen) { if (permissions.allowFullScreen) {
baseSandbox.push("allow-presentation"); baseSandbox.push("allow-presentation");
@@ -3,10 +3,11 @@
import { ActionIcon, Anchor, Avatar, Badge, Card, Group, Image, ScrollArea, Stack, Text, Tooltip } from "@mantine/core"; import { ActionIcon, Anchor, Avatar, Badge, Card, Group, Image, ScrollArea, Stack, Text, Tooltip } from "@mantine/core";
import { IconThumbDown, IconThumbUp } from "@tabler/icons-react"; import { IconThumbDown, IconThumbUp } from "@tabler/icons-react";
import type { RouterInputs, RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context"; import { useRequiredBoard } from "@homarr/boards/context";
import { MediaAvailability, MediaRequestStatus } from "@homarr/integrations/types"; import type { MediaRequestStatus } from "@homarr/integrations/types";
import type { ScopedTranslationFunction } from "@homarr/translation"; import { mediaAvailabilityConfiguration, mediaRequestStatusConfiguration } from "@homarr/integrations/types";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../../definition"; import type { WidgetComponentProps } from "../../definition";
@@ -18,7 +19,6 @@ export default function MediaServerWidget({
options, options,
width, width,
}: WidgetComponentProps<"mediaRequests-requestList">) { }: WidgetComponentProps<"mediaRequests-requestList">) {
const t = useScopedI18n("widget.mediaRequests-requestList");
const [mediaRequests] = clientApi.widget.mediaRequests.getLatestRequests.useSuspenseQuery( const [mediaRequests] = clientApi.widget.mediaRequests.getLatestRequests.useSuspenseQuery(
{ {
integrationIds, integrationIds,
@@ -48,20 +48,18 @@ export default function MediaServerWidget({
return dataB.createdAt.getTime() - dataA.createdAt.getTime(); return dataB.createdAt.getTime() - dataA.createdAt.getTime();
} }
return dataA.status - dataB.status; return (
mediaRequestStatusConfiguration[dataA.status].position -
mediaRequestStatusConfiguration[dataB.status].position
);
}); });
}); });
}, },
}, },
); );
const { mutate: mutateRequestAnswer } = clientApi.widget.mediaRequests.answerRequest.useMutation();
const board = useRequiredBoard();
if (mediaRequests.length === 0) throw new NoIntegrationDataError(); if (mediaRequests.length === 0) throw new NoIntegrationDataError();
const isTiny = width < 256;
return ( return (
<ScrollArea <ScrollArea
className="mediaRequests-list-scrollArea" className="mediaRequests-list-scrollArea"
@@ -70,193 +68,188 @@ export default function MediaServerWidget({
> >
<Stack className="mediaRequests-list-list" gap="xs" p="sm"> <Stack className="mediaRequests-list-list" gap="xs" p="sm">
{mediaRequests.map((mediaRequest) => ( {mediaRequests.map((mediaRequest) => (
<Card <MediaRequestCard
className={`mediaRequests-list-item-wrapper mediaRequests-list-item-${mediaRequest.type} mediaRequests-list-item-${mediaRequest.status}`}
key={`${mediaRequest.integrationId}-${mediaRequest.id}`} key={`${mediaRequest.integrationId}-${mediaRequest.id}`}
radius={board.itemRadius} request={mediaRequest}
p="xs" isTiny={width <= 256}
withBorder options={options}
> />
<Image
className="mediaRequests-list-item-background"
src={mediaRequest.backdropImageUrl}
pos="absolute"
w="100%"
h="100%"
opacity={0.2}
top={0}
left={0}
alt=""
/>
<Group
className="mediaRequests-list-item-contents"
h="100%"
style={{ zIndex: 1 }}
justify="space-between"
wrap="nowrap"
gap={0}
>
<Group className="mediaRequests-list-item-left-side" h="100%" gap="md" wrap="nowrap" flex={1}>
{!isTiny && (
<Image
className="mediaRequests-list-item-poster"
src={mediaRequest.posterImagePath}
h={40}
w="auto"
radius={"md"}
/>
)}
<Stack gap={0} w="100%">
<Group justify="space-between" gap="xs" className="mediaRequests-list-item-top-group">
<Group gap="xs">
<Text className="mediaRequests-list-item-media-year" size="xs">
{mediaRequest.airDate?.getFullYear() ?? t("toBeDetermined")}
</Text>
{!isTiny && (
<Badge
className="mediaRequests-list-item-media-status"
color={getAvailabilityProperties(mediaRequest.availability, t).color}
variant="light"
size="xs"
>
{getAvailabilityProperties(mediaRequest.availability, t).label}
</Badge>
)}
</Group>
<Group className="mediaRequests-list-item-request-user" gap={4} wrap="nowrap">
<Avatar
className="mediaRequests-list-item-request-user-avatar"
src={mediaRequest.requestedBy?.avatar}
size="xs"
/>
<Anchor
className="mediaRequests-list-item-request-user-name"
href={mediaRequest.requestedBy?.link}
c="var(--mantine-color-text)"
target={options.linksTargetNewTab ? "_blank" : "_self"}
fz="xs"
lineClamp={1}
style={{ wordBreak: "break-all" }}
>
{(mediaRequest.requestedBy?.displayName ?? "") || "unknown"}
</Anchor>
</Group>
</Group>
<Group gap="xs" justify="space-between" className="mediaRequests-list-item-bottom-group">
<Anchor
className="mediaRequests-list-item-info-second-line mediaRequests-list-item-media-title"
href={mediaRequest.href}
c="var(--mantine-color-text)"
target={options.linksTargetNewTab ? "_blank" : "_self"}
fz={isTiny ? "xs" : "sm"}
fw={"bold"}
title={mediaRequest.name}
lineClamp={1}
>
{mediaRequest.name || "unknown"}
</Anchor>
{mediaRequest.status === MediaRequestStatus.PendingApproval ? (
<Group className="mediaRequests-list-item-pending-buttons" gap="sm">
<Tooltip label={t("pending.approve")}>
<ActionIcon
className="mediaRequests-list-item-pending-button-approve"
variant="light"
color="green"
size="xs"
radius="md"
onClick={() => {
mutateRequestAnswer({
integrationId: mediaRequest.integrationId,
requestId: mediaRequest.id,
answer: "approve",
});
}}
>
<IconThumbUp size={16} />
</ActionIcon>
</Tooltip>
<Tooltip label={t("pending.decline")}>
<ActionIcon
className="mediaRequests-list-item-pending-button-decline"
variant="light"
color="red"
size="xs"
radius="md"
onClick={() => {
mutateRequestAnswer({
integrationId: mediaRequest.integrationId,
requestId: mediaRequest.id,
answer: "decline",
});
}}
>
<IconThumbDown size={16} />
</ActionIcon>
</Tooltip>
</Group>
) : (
<StatusBadge status={mediaRequest.status} />
)}
</Group>
</Stack>
</Group>
</Group>
</Card>
))} ))}
</Stack> </Stack>
</ScrollArea> </ScrollArea>
); );
} }
const statusMapping = { interface MediaRequestCardProps {
[MediaRequestStatus.PendingApproval]: { color: "blue", label: (t) => t("pending") }, request: RouterOutputs["widget"]["mediaRequests"]["getLatestRequests"][number];
[MediaRequestStatus.Approved]: { color: "green", label: (t) => t("approved") }, isTiny: boolean;
[MediaRequestStatus.Declined]: { color: "red", label: (t) => t("declined") }, options: WidgetComponentProps<"mediaRequests-requestList">["options"];
[MediaRequestStatus.Failed]: { color: "red", label: (t) => t("failed") }, }
[MediaRequestStatus.Completed]: { color: "green", label: (t) => t("completed") },
} satisfies Record< const MediaRequestCard = ({ request, isTiny, options }: MediaRequestCardProps) => {
MediaRequestStatus, const board = useRequiredBoard();
{ const t = useScopedI18n("widget.mediaRequests-requestList");
color: string;
label: (t: ScopedTranslationFunction<"widget.mediaRequests-requestList.status">) => string; return (
} <Card
>; className={`mediaRequests-list-item-wrapper mediaRequests-list-item-${request.type} mediaRequests-list-item-${request.status}`}
radius={board.itemRadius}
p="xs"
withBorder
>
<Image
className="mediaRequests-list-item-background"
src={request.backdropImageUrl}
pos="absolute"
w="100%"
h="100%"
opacity={0.2}
top={0}
left={0}
alt=""
/>
<Group
className="mediaRequests-list-item-contents"
h="100%"
style={{ zIndex: 1 }}
justify="space-between"
wrap="nowrap"
gap={0}
>
<Group className="mediaRequests-list-item-left-side" h="100%" gap="md" wrap="nowrap" flex={1}>
{!isTiny && (
<Image
className="mediaRequests-list-item-poster"
src={request.posterImagePath}
h={40}
w="auto"
radius={"md"}
/>
)}
<Stack gap={0} w="100%">
<Group justify="space-between" gap="xs" className="mediaRequests-list-item-top-group">
<Group gap="xs">
<Text className="mediaRequests-list-item-media-year" size="xs">
{request.airDate?.getFullYear() ?? t("toBeDetermined")}
</Text>
{!isTiny && (
<Badge
className="mediaRequests-list-item-media-status"
color={mediaAvailabilityConfiguration[request.availability].color}
variant="light"
size="xs"
>
{t(`availability.${request.availability}`)}
</Badge>
)}
</Group>
<Group className="mediaRequests-list-item-request-user" gap={4} wrap="nowrap">
<Avatar
className="mediaRequests-list-item-request-user-avatar"
src={request.requestedBy?.avatar}
size="xs"
/>
<Anchor
className="mediaRequests-list-item-request-user-name"
href={request.requestedBy?.link}
c="var(--mantine-color-text)"
target={options.linksTargetNewTab ? "_blank" : "_self"}
fz="xs"
lineClamp={1}
style={{ wordBreak: "break-all" }}
>
{(request.requestedBy?.displayName ?? "") || "unknown"}
</Anchor>
</Group>
</Group>
<Group gap="xs" justify="space-between" className="mediaRequests-list-item-bottom-group">
<Anchor
className="mediaRequests-list-item-info-second-line mediaRequests-list-item-media-title"
href={request.href}
c="var(--mantine-color-text)"
target={options.linksTargetNewTab ? "_blank" : "_self"}
fz={isTiny ? "xs" : "sm"}
fw={"bold"}
title={request.name}
lineClamp={1}
>
{request.name || "unknown"}
</Anchor>
{request.status === "pending" ? (
<DecisionButtons requestId={request.id} integrationId={request.integrationId} />
) : (
<StatusBadge status={request.status} />
)}
</Group>
</Stack>
</Group>
</Group>
</Card>
);
};
interface DecisionButtonsProps {
requestId: number;
integrationId: string;
}
const DecisionButtons = ({ requestId, integrationId }: DecisionButtonsProps) => {
const { mutate: mutateRequestAnswer } = clientApi.widget.mediaRequests.answerRequest.useMutation();
const t = useScopedI18n("widget.mediaRequests-requestList");
const handleDecision = (answer: RouterInputs["widget"]["mediaRequests"]["answerRequest"]["answer"]) => {
mutateRequestAnswer({
integrationId,
requestId,
answer,
});
};
return (
<Group className="mediaRequests-list-item-pending-buttons" gap="sm">
<Tooltip label={t("pending.approve")}>
<ActionIcon
className="mediaRequests-list-item-pending-button-approve"
variant="light"
color="green"
size="xs"
radius="md"
onClick={() => {
handleDecision("approve");
}}
>
<IconThumbUp size={16} />
</ActionIcon>
</Tooltip>
<Tooltip label={t("pending.decline")}>
<ActionIcon
className="mediaRequests-list-item-pending-button-decline"
variant="light"
color="red"
size="xs"
radius="md"
onClick={() => {
handleDecision("decline");
}}
>
<IconThumbDown size={16} />
</ActionIcon>
</Tooltip>
</Group>
);
};
interface StatusBadgeProps { interface StatusBadgeProps {
status: MediaRequestStatus; status: MediaRequestStatus;
} }
const StatusBadge = ({ status }: StatusBadgeProps) => { const StatusBadge = ({ status }: StatusBadgeProps) => {
const { color, label } = statusMapping[status];
const tStatus = useScopedI18n("widget.mediaRequests-requestList.status"); const tStatus = useScopedI18n("widget.mediaRequests-requestList.status");
return ( return (
<Badge size="xs" color={color} variant="light"> <Badge size="xs" color={mediaRequestStatusConfiguration[status].color} variant="light">
{label(tStatus)} {tStatus(status)}
</Badge> </Badge>
); );
}; };
function getAvailabilityProperties(
mediaRequestAvailability: MediaAvailability,
t: ScopedTranslationFunction<"widget.mediaRequests-requestList">,
) {
switch (mediaRequestAvailability) {
case MediaAvailability.Available:
return { color: "green", label: t("availability.available") };
case MediaAvailability.PartiallyAvailable:
return { color: "yellow", label: t("availability.partiallyAvailable") };
case MediaAvailability.Processing:
return { color: "blue", label: t("availability.processing") };
case MediaAvailability.Pending:
return { color: "violet", label: t("availability.pending") };
case MediaAvailability.Blacklisted:
return { color: "gray", label: t("availability.blacklisted") };
case MediaAvailability.Deleted:
return { color: "red", label: t("availability.deleted") };
default:
return { color: "orange", label: t("availability.unknown") };
}
}
+917 -827
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -17,9 +17,9 @@
}, },
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@next/eslint-plugin-next": "16.0.1", "@next/eslint-plugin-next": "16.0.3",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-config-turbo": "^2.6.0", "eslint-config-turbo": "^2.6.1",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
+1 -1
View File
@@ -7,7 +7,7 @@ runs:
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6 - uses: actions/setup-node@v6
with: with:
node-version: 24.11.0 node-version: 24.11.1
cache: "pnpm" cache: "pnpm"
- shell: bash - shell: bash