chore(release): automatic release v0.1.0
This commit is contained in:
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1 +1 @@
|
|||||||
* text eol=lf
|
* text=auto eol=lf
|
||||||
40
.github/workflows/crowdin-schedule-download.yml
vendored
Normal file
40
.github/workflows/crowdin-schedule-download.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: "[Crowdin] Download translations"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * *" # every day at midnight
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
download-crowdin-translations:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Obtain token
|
||||||
|
id: obtainToken
|
||||||
|
uses: tibdex/github-app-token@v2
|
||||||
|
with:
|
||||||
|
private_key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }}
|
||||||
|
app_id: ${{ secrets.CROWDIN_APP_ID }}
|
||||||
|
|
||||||
|
- name: Download Crowdin translations
|
||||||
|
uses: crowdin/github-action@v2
|
||||||
|
with:
|
||||||
|
upload_sources: false
|
||||||
|
upload_translations: false
|
||||||
|
download_translations: true
|
||||||
|
localization_branch_name: crowdin
|
||||||
|
create_pull_request: true
|
||||||
|
pull_request_title: "chore(lang): updated translations from crowdin"
|
||||||
|
pull_request_body: "New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)"
|
||||||
|
pull_request_base_branch_name: "dev"
|
||||||
|
github_user_name: "Crowdin Homarr"
|
||||||
|
github_user_email: "190541745+homarr-crowdin[bot]@users.noreply.github.com"
|
||||||
|
skip_untranslated_strings: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||||
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
28
.github/workflows/crowdin-upload.yml
vendored
Normal file
28
.github/workflows/crowdin-upload.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: "[Crowdin] Upload translations"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "packages/translation/src/lang/**"
|
||||||
|
branches: [dev]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
upload-crowdin-translations:
|
||||||
|
# Don't run this action if the downloaded translations are being pushed
|
||||||
|
if: "!contains(github.event.head_commit.message, 'chore(lang)')"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Upload Crowdin translations
|
||||||
|
uses: crowdin/github-action@v2
|
||||||
|
with:
|
||||||
|
upload_sources: true
|
||||||
|
upload_translations: true
|
||||||
|
download_translations: false
|
||||||
|
env:
|
||||||
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
55
Dockerfile
55
Dockerfile
@@ -1,4 +1,4 @@
|
|||||||
FROM node:22.11.0-alpine AS base
|
FROM node:22.12.0-alpine AS base
|
||||||
|
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
@@ -6,46 +6,15 @@ RUN apk update
|
|||||||
|
|
||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . .
|
|
||||||
RUN npm i -g turbo
|
|
||||||
RUN turbo prune @homarr/nextjs --docker --out-dir ./next-out
|
|
||||||
RUN turbo prune @homarr/tasks --docker --out-dir ./tasks-out
|
|
||||||
RUN turbo prune @homarr/websocket --docker --out-dir ./websocket-out
|
|
||||||
RUN turbo prune @homarr/db --docker --out-dir ./migration-out
|
|
||||||
RUN turbo prune @homarr/cli --docker --out-dir ./cli-out
|
|
||||||
|
|
||||||
# Add lockfile and package.json's of isolated subworkspace
|
|
||||||
FROM base AS installer
|
|
||||||
RUN apk add --no-cache libc6-compat curl bash
|
RUN apk add --no-cache libc6-compat curl bash
|
||||||
RUN apk update
|
RUN apk update
|
||||||
WORKDIR /app
|
COPY . .
|
||||||
|
|
||||||
# First install the dependencies (as they change less often)
|
|
||||||
COPY .gitignore .gitignore
|
|
||||||
|
|
||||||
COPY --from=builder /app/tasks-out/json/ .
|
|
||||||
COPY --from=builder /app/websocket-out/json/ .
|
|
||||||
COPY --from=builder /app/migration-out/json/ .
|
|
||||||
COPY --from=builder /app/cli-out/json/ .
|
|
||||||
COPY --from=builder /app/next-out/json/ .
|
|
||||||
COPY --from=builder /app/pnpm-lock.yaml ./pnpm-lock.yaml
|
|
||||||
|
|
||||||
# Is used for postinstall of docs definitions
|
|
||||||
COPY --from=builder /app/packages/definitions/src/docs ./packages/definitions/src/docs
|
|
||||||
|
|
||||||
# Uses the lockfile to install the dependencies
|
|
||||||
RUN corepack enable pnpm && pnpm install --recursive --frozen-lockfile
|
RUN corepack enable pnpm && pnpm install --recursive --frozen-lockfile
|
||||||
|
|
||||||
# Install sharp for image optimization
|
# Install sharp for image optimization
|
||||||
RUN corepack enable pnpm && pnpm install sharp -w
|
RUN corepack enable pnpm && pnpm install sharp -w
|
||||||
|
|
||||||
# Build the project
|
|
||||||
COPY --from=builder /app/tasks-out/full/ .
|
|
||||||
COPY --from=builder /app/websocket-out/full/ .
|
|
||||||
COPY --from=builder /app/next-out/full/ .
|
|
||||||
COPY --from=builder /app/migration-out/full/ .
|
|
||||||
COPY --from=builder /app/cli-out/full/ .
|
|
||||||
|
|
||||||
# Copy static data as it is not part of the build
|
# Copy static data as it is not part of the build
|
||||||
COPY static-data ./static-data
|
COPY static-data ./static-data
|
||||||
ARG SKIP_ENV_VALIDATION='true'
|
ARG SKIP_ENV_VALIDATION='true'
|
||||||
@@ -69,7 +38,7 @@ RUN addgroup --system --gid 1001 nodejs
|
|||||||
RUN adduser --system --uid 1001 nextjs
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
# Enable homarr cli
|
# Enable homarr cli
|
||||||
COPY --from=installer --chown=nextjs:nodejs /app/packages/cli/cli.cjs /app/apps/cli/cli.cjs
|
COPY --from=builder --chown=nextjs:nodejs /app/packages/cli/cli.cjs /app/apps/cli/cli.cjs
|
||||||
RUN echo $'#!/bin/bash\ncd /app/apps/cli && node ./cli.cjs "$@"' > /usr/bin/homarr
|
RUN echo $'#!/bin/bash\ncd /app/apps/cli && node ./cli.cjs "$@"' > /usr/bin/homarr
|
||||||
RUN chmod +x /usr/bin/homarr
|
RUN chmod +x /usr/bin/homarr
|
||||||
|
|
||||||
@@ -83,20 +52,20 @@ RUN mkdir -p /var/cache/nginx && chown -R nextjs:nodejs /var/cache/nginx && \
|
|||||||
mkdir -p /etc/nginx/templates /etc/nginx/ssl/certs && chown -R nextjs:nodejs /etc/nginx
|
mkdir -p /etc/nginx/templates /etc/nginx/ssl/certs && chown -R nextjs:nodejs /etc/nginx
|
||||||
USER nextjs
|
USER nextjs
|
||||||
|
|
||||||
COPY --from=installer /app/apps/nextjs/next.config.mjs .
|
COPY --from=builder /app/apps/nextjs/next.config.mjs .
|
||||||
COPY --from=installer /app/apps/nextjs/package.json .
|
COPY --from=builder /app/apps/nextjs/package.json .
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nodejs /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
|
COPY --from=builder --chown=nextjs:nodejs /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
|
||||||
COPY --from=installer --chown=nextjs:nodejs /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
|
COPY --from=builder --chown=nextjs:nodejs /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
|
||||||
COPY --from=installer --chown=nextjs:nodejs /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
|
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nodejs /app/packages/db/migrations ./db/migrations
|
COPY --from=builder --chown=nextjs:nodejs /app/packages/db/migrations ./db/migrations
|
||||||
|
|
||||||
# Automatically leverage output traces to reduce image size
|
# Automatically leverage output traces to reduce image size
|
||||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||||
COPY --from=installer --chown=nextjs:nodejs /app/apps/nextjs/.next/standalone ./
|
COPY --from=builder --chown=nextjs:nodejs /app/apps/nextjs/.next/standalone ./
|
||||||
COPY --from=installer --chown=nextjs:nodejs /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
|
COPY --from=builder --chown=nextjs:nodejs /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
|
||||||
COPY --from=installer --chown=nextjs:nodejs /app/apps/nextjs/public ./apps/nextjs/public
|
COPY --from=builder --chown=nextjs:nodejs /app/apps/nextjs/public ./apps/nextjs/public
|
||||||
COPY --chown=nextjs:nodejs scripts/run.sh ./run.sh
|
COPY --chown=nextjs:nodejs scripts/run.sh ./run.sh
|
||||||
COPY --chown=nextjs:nodejs scripts/generateEncryptionKey.js ./generateEncryptionKey.js
|
COPY --chown=nextjs:nodejs scripts/generateEncryptionKey.js ./generateEncryptionKey.js
|
||||||
COPY --chown=nextjs:nodejs packages/redis/redis.conf /app/redis.conf
|
COPY --chown=nextjs:nodejs packages/redis/redis.conf /app/redis.conf
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/form": "workspace:^0.1.0",
|
"@homarr/form": "workspace:^0.1.0",
|
||||||
"@homarr/gridstack": "^1.0.3",
|
"@homarr/gridstack": "^1.11.2",
|
||||||
"@homarr/integrations": "workspace:^0.1.0",
|
"@homarr/integrations": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^",
|
"@homarr/log": "workspace:^",
|
||||||
"@homarr/modals": "workspace:^0.1.0",
|
"@homarr/modals": "workspace:^0.1.0",
|
||||||
@@ -44,10 +44,10 @@
|
|||||||
"@mantine/tiptap": "^7.14.3",
|
"@mantine/tiptap": "^7.14.3",
|
||||||
"@million/lint": "1.0.13",
|
"@million/lint": "1.0.13",
|
||||||
"@t3-oss/env-nextjs": "^0.11.1",
|
"@t3-oss/env-nextjs": "^0.11.1",
|
||||||
"@tabler/icons-react": "^3.23.0",
|
"@tabler/icons-react": "^3.24.0",
|
||||||
"@tanstack/react-query": "^5.62.0",
|
"@tanstack/react-query": "^5.62.3",
|
||||||
"@tanstack/react-query-devtools": "^5.62.0",
|
"@tanstack/react-query-devtools": "^5.62.3",
|
||||||
"@tanstack/react-query-next-experimental": "5.62.0",
|
"@tanstack/react-query-next-experimental": "5.62.3",
|
||||||
"@trpc/client": "next",
|
"@trpc/client": "next",
|
||||||
"@trpc/next": "next",
|
"@trpc/next": "next",
|
||||||
"@trpc/react-query": "next",
|
"@trpc/react-query": "next",
|
||||||
@@ -58,20 +58,20 @@
|
|||||||
"chroma-js": "^3.1.2",
|
"chroma-js": "^3.1.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.7",
|
||||||
"flag-icons": "^7.2.3",
|
"flag-icons": "^7.2.3",
|
||||||
"glob": "^11.0.0",
|
"glob": "^11.0.0",
|
||||||
"jotai": "^2.10.3",
|
"jotai": "^2.10.3",
|
||||||
"mantine-react-table": "2.0.0-beta.7",
|
"mantine-react-table": "2.0.0-beta.7",
|
||||||
"next": "^14.2.18",
|
"next": "^14.2.20",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"prismjs": "^1.29.0",
|
"prismjs": "^1.29.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-error-boundary": "^4.1.2",
|
"react-error-boundary": "^4.1.2",
|
||||||
"react-simple-code-editor": "^0.14.1",
|
"react-simple-code-editor": "^0.14.1",
|
||||||
"sass": "^1.81.0",
|
"sass": "^1.82.0",
|
||||||
"superjson": "2.2.1",
|
"superjson": "2.2.2",
|
||||||
"swagger-ui-react": "^5.18.2",
|
"swagger-ui-react": "^5.18.2",
|
||||||
"use-deep-compare-effect": "^1.8.1"
|
"use-deep-compare-effect": "^1.8.1"
|
||||||
},
|
},
|
||||||
@@ -82,13 +82,13 @@
|
|||||||
"@types/chroma-js": "2.4.4",
|
"@types/chroma-js": "2.4.4",
|
||||||
"@types/node": "^22.10.1",
|
"@types/node": "^22.10.1",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.13",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@types/swagger-ui-react": "^4.18.3",
|
"@types/swagger-ui-react": "^4.18.3",
|
||||||
"concurrently": "^9.1.0",
|
"concurrently": "^9.1.0",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"node-loader": "^2.1.0",
|
"node-loader": "^2.1.0",
|
||||||
"prettier": "^3.4.1",
|
"prettier": "^3.4.2",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { env } from "@homarr/auth/env.mjs";
|
|||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { ModalProvider } from "@homarr/modals";
|
import { ModalProvider } from "@homarr/modals";
|
||||||
import { Notifications } from "@homarr/notifications";
|
import { Notifications } from "@homarr/notifications";
|
||||||
|
import { SpotlightProvider } from "@homarr/spotlight";
|
||||||
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
|
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
|
||||||
import { getI18nMessages } from "@homarr/translation/server";
|
import { getI18nMessages } from "@homarr/translation/server";
|
||||||
|
|
||||||
@@ -82,6 +83,7 @@ export default async function Layout(props: { children: React.ReactNode; params:
|
|||||||
(innerProps) => <NextIntlClientProvider {...innerProps} messages={i18nMessages} />,
|
(innerProps) => <NextIntlClientProvider {...innerProps} messages={i18nMessages} />,
|
||||||
(innerProps) => <CustomMantineProvider {...innerProps} defaultColorScheme={colorScheme} />,
|
(innerProps) => <CustomMantineProvider {...innerProps} defaultColorScheme={colorScheme} />,
|
||||||
(innerProps) => <ModalProvider {...innerProps} />,
|
(innerProps) => <ModalProvider {...innerProps} />,
|
||||||
|
(innerProps) => <SpotlightProvider {...innerProps} />,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -36,9 +36,9 @@
|
|||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@homarr/widgets": "workspace:^0.1.0",
|
"@homarr/widgets": "workspace:^0.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.7",
|
||||||
"superjson": "2.2.1",
|
"superjson": "2.2.2",
|
||||||
"undici": "7.0.0"
|
"undici": "7.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
@@ -46,8 +46,8 @@
|
|||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node": "^22.10.1",
|
"@types/node": "^22.10.1",
|
||||||
"dotenv-cli": "^7.4.4",
|
"dotenv-cli": "^7.4.4",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"prettier": "^3.4.1",
|
"prettier": "^3.4.2",
|
||||||
"tsx": "4.19.2",
|
"tsx": "4.19.2",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"@homarr/log": "workspace:^",
|
"@homarr/log": "workspace:^",
|
||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.7",
|
||||||
"tsx": "4.19.2",
|
"tsx": "4.19.2",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
"@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.5.13",
|
"@types/ws": "^8.5.13",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"prettier": "^3.4.1",
|
"prettier": "^3.4.2",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
crowdin.yml
Normal file
10
crowdin.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
files:
|
||||||
|
- source: /packages/translation/src/lang/en.json
|
||||||
|
translation: /packages/translation/src/lang/%two_letters_code%.json
|
||||||
|
|
||||||
|
# Title of pull request and so the commit that will be used for squashed merge commit
|
||||||
|
pull_request_title: "chore(lang): updated translations from crowdin"
|
||||||
|
|
||||||
|
# Custom commit message that is not only appended
|
||||||
|
commit_message: "chore(lang): update translations %original_file_name% from crowdin [skip ci]"
|
||||||
|
append_commit_message: false
|
||||||
21
package.json
21
package.json
@@ -31,19 +31,24 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@turbo/gen": "^2.3.3",
|
"@turbo/gen": "^2.3.3",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"@vitest/coverage-v8": "^2.1.6",
|
"@vitest/coverage-v8": "^2.1.8",
|
||||||
"@vitest/ui": "^2.1.6",
|
"@vitest/ui": "^2.1.8",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"jsdom": "^25.0.1",
|
"jsdom": "^25.0.1",
|
||||||
"prettier": "^3.4.1",
|
"prettier": "^3.4.2",
|
||||||
"testcontainers": "^10.15.0",
|
"testcontainers": "^10.16.0",
|
||||||
"turbo": "^2.3.3",
|
"turbo": "^2.3.3",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vite-tsconfig-paths": "^5.1.3",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^2.1.6"
|
"vitest": "^2.1.8"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.14.4",
|
"packageManager": "pnpm@9.15.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.11.0"
|
"node": ">=22.12.0"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"patchedDependencies": {
|
||||||
|
"pretty-print-error": "patches/pretty-print-error.patch"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,13 +26,13 @@
|
|||||||
"@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",
|
||||||
"@umami/node": "^0.4.0",
|
"@umami/node": "^0.4.0",
|
||||||
"superjson": "2.2.1"
|
"superjson": "2.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,18 +40,18 @@
|
|||||||
"@trpc/react-query": "next",
|
"@trpc/react-query": "next",
|
||||||
"@trpc/server": "next",
|
"@trpc/server": "next",
|
||||||
"dockerode": "^4.0.2",
|
"dockerode": "^4.0.2",
|
||||||
"next": "^14.2.18",
|
"next": "^14.2.20",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"superjson": "2.2.1",
|
"superjson": "2.2.2",
|
||||||
"trpc-to-openapi": "^2.0.2"
|
"trpc-to-openapi": "^2.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@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.32",
|
"@types/dockerode": "^3.3.32",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"prettier": "^3.4.1",
|
"prettier": "^3.4.2",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,8 +33,8 @@
|
|||||||
"@t3-oss/env-nextjs": "^0.11.1",
|
"@t3-oss/env-nextjs": "^0.11.1",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"cookies": "^0.9.1",
|
"cookies": "^0.9.1",
|
||||||
"ldapts": "7.2.1",
|
"ldapts": "7.2.2",
|
||||||
"next": "^14.2.18",
|
"next": "^14.2.20",
|
||||||
"next-auth": "5.0.0-beta.25",
|
"next-auth": "5.0.0-beta.25",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1"
|
||||||
@@ -45,8 +45,8 @@
|
|||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/bcrypt": "5.0.2",
|
"@types/bcrypt": "5.0.2",
|
||||||
"@types/cookies": "0.9.0",
|
"@types/cookies": "0.9.0",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"prettier": "^3.4.1",
|
"prettier": "^3.4.2",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const authorizeWithBasicCredentialsAsync = async (
|
|||||||
credentials: z.infer<typeof validation.user.signIn>,
|
credentials: z.infer<typeof validation.user.signIn>,
|
||||||
) => {
|
) => {
|
||||||
const user = await db.query.users.findFirst({
|
const user = await db.query.users.findFirst({
|
||||||
where: and(eq(users.name, credentials.name), eq(users.provider, "credentials")),
|
where: and(eq(users.name, credentials.name.toLowerCase()), eq(users.provider, "credentials")),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user?.password) {
|
if (!user?.password) {
|
||||||
|
|||||||
@@ -27,13 +27,13 @@
|
|||||||
"@homarr/auth": "workspace:^0.1.0",
|
"@homarr/auth": "workspace:^0.1.0",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"dotenv": "^16.4.5"
|
"dotenv": "^16.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,15 +27,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"next": "^14.2.18",
|
"next": "^14.2.20",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"tldts": "^6.1.64"
|
"tldts": "^6.1.65"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { parseAppHrefWithVariables } from "./base";
|
import { parseAppHrefWithVariables } from "./base";
|
||||||
|
|
||||||
export const parseAppHrefWithVariablesClient = <TInput extends string | null>(url: TInput): TInput => {
|
export const parseAppHrefWithVariablesClient = <TInput extends string | null>(url: TInput): TInput => {
|
||||||
|
if (typeof window === "undefined") return url;
|
||||||
return parseAppHrefWithVariables(url, window.location.href);
|
return parseAppHrefWithVariables(url, window.location.href);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
|
import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
|
||||||
|
|
||||||
export const appendPath = (url: URL | string, path: string) => {
|
export const removeTrailingSlash = (path: string) => {
|
||||||
const newUrl = new URL(url);
|
|
||||||
newUrl.pathname = removeTrailingSlash(newUrl.pathname) + path;
|
|
||||||
return newUrl;
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeTrailingSlash = (path: string) => {
|
|
||||||
return path.at(-1) === "/" ? path.substring(0, path.length - 1) : path;
|
return path.at(-1) === "/" ? path.substring(0, path.length - 1) : path;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,12 @@
|
|||||||
"clean": "rm -rf .turbo node_modules",
|
"clean": "rm -rf .turbo node_modules",
|
||||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
|
"migration:mysql:drop": "drizzle-kit drop --config ./configs/mysql.config.ts",
|
||||||
"migration:mysql:generate": "drizzle-kit generate --config ./configs/mysql.config.ts",
|
"migration:mysql:generate": "drizzle-kit generate --config ./configs/mysql.config.ts",
|
||||||
"migration:mysql:run": "drizzle-kit migrate --config ./configs/mysql.config.ts && pnpm run seed",
|
"migration:mysql:run": "drizzle-kit migrate --config ./configs/mysql.config.ts && pnpm run seed",
|
||||||
"migration:mysql:drop": "drizzle-kit drop --config ./configs/mysql.config.ts",
|
"migration:sqlite:drop": "drizzle-kit drop --config ./configs/sqlite.config.ts",
|
||||||
"migration:sqlite:generate": "drizzle-kit generate --config ./configs/sqlite.config.ts",
|
"migration:sqlite:generate": "drizzle-kit generate --config ./configs/sqlite.config.ts",
|
||||||
"migration:sqlite:run": "drizzle-kit migrate --config ./configs/sqlite.config.ts && pnpm run seed",
|
"migration:sqlite:run": "drizzle-kit migrate --config ./configs/sqlite.config.ts && pnpm run seed",
|
||||||
"migration:sqlite:drop": "drizzle-kit drop --config ./configs/sqlite.config.ts",
|
|
||||||
"push:mysql": "drizzle-kit push --config ./configs/mysql.config.ts",
|
"push:mysql": "drizzle-kit push --config ./configs/mysql.config.ts",
|
||||||
"push:sqlite": "drizzle-kit push --config ./configs/sqlite.config.ts",
|
"push:sqlite": "drizzle-kit push --config ./configs/sqlite.config.ts",
|
||||||
"seed": "pnpm with-env tsx ./migrations/run-seed.ts",
|
"seed": "pnpm with-env tsx ./migrations/run-seed.ts",
|
||||||
@@ -42,11 +42,11 @@
|
|||||||
"@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",
|
||||||
"@paralleldrive/cuid2": "^2.2.2",
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
"@testcontainers/mysql": "^10.15.0",
|
"@testcontainers/mysql": "^10.16.0",
|
||||||
"better-sqlite3": "^11.6.0",
|
"better-sqlite3": "^11.6.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.7",
|
||||||
"drizzle-kit": "^0.28.1",
|
"drizzle-kit": "^0.29.1",
|
||||||
"drizzle-orm": "^0.36.4",
|
"drizzle-orm": "^0.37.0",
|
||||||
"drizzle-zod": "^0.5.1",
|
"drizzle-zod": "^0.5.1",
|
||||||
"mysql2": "3.11.5"
|
"mysql2": "3.11.5"
|
||||||
},
|
},
|
||||||
@@ -56,8 +56,8 @@
|
|||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/better-sqlite3": "7.6.12",
|
"@types/better-sqlite3": "7.6.12",
|
||||||
"dotenv-cli": "^7.4.4",
|
"dotenv-cli": "^7.4.4",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"prettier": "^3.4.1",
|
"prettier": "^3.4.2",
|
||||||
"tsx": "4.19.2",
|
"tsx": "4.19.2",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -rf .turbo node_modules",
|
"clean": "rm -rf .turbo node_modules",
|
||||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
|
"postinstall": "tsx ./src/docs/codegen.ts",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit"
|
||||||
"postinstall": "tsx ./src/docs/codegen.ts"
|
|
||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -29,7 +29,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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@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/redis": "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",
|
||||||
"@jellyfin/sdk": "^0.11.0",
|
"@jellyfin/sdk": "^0.11.0",
|
||||||
@@ -41,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/xml2js": "^0.4.14",
|
"@types/xml2js": "^0.4.14",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { filteringStatusSchema, statsResponseSchema, statusResponseSchema } from
|
|||||||
|
|
||||||
export class AdGuardHomeIntegration extends Integration implements DnsHoleSummaryIntegration {
|
export class AdGuardHomeIntegration extends Integration implements DnsHoleSummaryIntegration {
|
||||||
public async getSummaryAsync(): Promise<DnsHoleSummary> {
|
public async getSummaryAsync(): Promise<DnsHoleSummary> {
|
||||||
const statsResponse = await fetch(`${this.integration.url}/control/stats`, {
|
const statsResponse = await fetch(this.url("/control/stats"), {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
||||||
},
|
},
|
||||||
@@ -18,7 +18,7 @@ export class AdGuardHomeIntegration extends Integration implements DnsHoleSummar
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusResponse = await fetch(`${this.integration.url}/control/status`, {
|
const statusResponse = await fetch(this.url("/control/status"), {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
||||||
},
|
},
|
||||||
@@ -30,7 +30,7 @@ export class AdGuardHomeIntegration extends Integration implements DnsHoleSummar
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteringStatusResponse = await fetch(`${this.integration.url}/control/filtering/status`, {
|
const filteringStatusResponse = await fetch(this.url("/control/filtering/status"), {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
||||||
},
|
},
|
||||||
@@ -86,7 +86,7 @@ export class AdGuardHomeIntegration extends Integration implements DnsHoleSummar
|
|||||||
public async testConnectionAsync(): Promise<void> {
|
public async testConnectionAsync(): Promise<void> {
|
||||||
await super.handleTestConnectionResponseAsync({
|
await super.handleTestConnectionResponseAsync({
|
||||||
queryFunctionAsync: async () => {
|
queryFunctionAsync: async () => {
|
||||||
return await fetch(`${this.integration.url}/control/status`, {
|
return await fetch(this.url("/control/status"), {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
||||||
},
|
},
|
||||||
@@ -106,7 +106,7 @@ export class AdGuardHomeIntegration extends Integration implements DnsHoleSummar
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async enableAsync(): Promise<void> {
|
public async enableAsync(): Promise<void> {
|
||||||
const response = await fetch(`${this.integration.url}/control/protection`, {
|
const response = await fetch(this.url("/control/protection"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -124,7 +124,7 @@ export class AdGuardHomeIntegration extends Integration implements DnsHoleSummar
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async disableAsync(duration = 0): Promise<void> {
|
public async disableAsync(duration = 0): Promise<void> {
|
||||||
const response = await fetch(`${this.integration.url}/control/protection`, {
|
const response = await fetch(this.url("/control/protection"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { extractErrorMessage } from "@homarr/common";
|
import { extractErrorMessage, removeTrailingSlash } from "@homarr/common";
|
||||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
import type { IntegrationSecretKind } from "@homarr/definitions";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
import type { TranslationObject } from "@homarr/translation";
|
import type { TranslationObject } from "@homarr/translation";
|
||||||
@@ -29,6 +29,19 @@ export abstract class Integration {
|
|||||||
return secret.value;
|
return secret.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected url(path: `/${string}`, queryParams?: Record<string, string | Date | number | boolean>) {
|
||||||
|
const baseUrl = removeTrailingSlash(this.integration.url);
|
||||||
|
const url = new URL(`${baseUrl}${path}`);
|
||||||
|
|
||||||
|
if (queryParams) {
|
||||||
|
for (const [key, value] of Object.entries(queryParams)) {
|
||||||
|
url.searchParams.set(key, value instanceof Date ? value.toISOString() : value.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the connection to the integration
|
* Test the connection to the integration
|
||||||
* @throws {IntegrationTestConnectionError} if the connection fails
|
* @throws {IntegrationTestConnectionError} if the connection fails
|
||||||
|
|||||||
@@ -89,9 +89,8 @@ export class DelugeIntegration extends DownloadClientIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getClient() {
|
private getClient() {
|
||||||
const baseUrl = new URL(this.integration.url).href;
|
|
||||||
return new Deluge({
|
return new Deluge({
|
||||||
baseUrl,
|
baseUrl: this.url("/").toString(),
|
||||||
password: this.getSecretValue("password"),
|
password: this.getSecretValue("password"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,9 +92,9 @@ export class NzbGetIntegration extends DownloadClientIntegration {
|
|||||||
method: CallType,
|
method: CallType,
|
||||||
...params: Parameters<NzbGetClient[CallType]>
|
...params: Parameters<NzbGetClient[CallType]>
|
||||||
): Promise<ReturnType<NzbGetClient[CallType]>> {
|
): Promise<ReturnType<NzbGetClient[CallType]>> {
|
||||||
const url = new URL(this.integration.url);
|
const username = this.getSecretValue("username");
|
||||||
url.pathname += `${this.getSecretValue("username")}:${this.getSecretValue("password")}`;
|
const password = this.getSecretValue("password");
|
||||||
url.pathname += url.pathname.endsWith("/") ? "jsonrpc" : "/jsonrpc";
|
const url = this.url(`/${username}:${password}/jsonrpc`);
|
||||||
const body = JSON.stringify({ method, params });
|
const body = JSON.stringify({ method, params });
|
||||||
return await fetch(url, { method: "POST", body })
|
return await fetch(url, { method: "POST", body })
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
|
|||||||
@@ -70,9 +70,8 @@ export class QBitTorrentIntegration extends DownloadClientIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getClient() {
|
private getClient() {
|
||||||
const baseUrl = new URL(this.integration.url).href;
|
|
||||||
return new QBittorrent({
|
return new QBittorrent({
|
||||||
baseUrl,
|
baseUrl: this.url("/").toString(),
|
||||||
username: this.getSecretValue("username"),
|
username: this.getSecretValue("username"),
|
||||||
password: this.getSecretValue("password"),
|
password: this.getSecretValue("password"),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ dayjs.extend(duration);
|
|||||||
export class SabnzbdIntegration extends DownloadClientIntegration {
|
export class SabnzbdIntegration extends DownloadClientIntegration {
|
||||||
public async testConnectionAsync(): Promise<void> {
|
public async testConnectionAsync(): Promise<void> {
|
||||||
//This is the one call that uses the least amount of data while requiring the api key
|
//This is the one call that uses the least amount of data while requiring the api key
|
||||||
await this.sabNzbApiCallAsync("translate", new URLSearchParams({ value: "ping" }));
|
await this.sabNzbApiCallAsync("translate", { value: "ping" });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
||||||
@@ -75,7 +75,7 @@ export class SabnzbdIntegration extends DownloadClientIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async pauseItemAsync({ id }: DownloadClientItem) {
|
public async pauseItemAsync({ id }: DownloadClientItem) {
|
||||||
await this.sabNzbApiCallAsync("queue", new URLSearchParams({ name: "pause", value: id }));
|
await this.sabNzbApiCallAsync("queue", { name: "pause", value: id });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async resumeQueueAsync() {
|
public async resumeQueueAsync() {
|
||||||
@@ -83,32 +83,29 @@ export class SabnzbdIntegration extends DownloadClientIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async resumeItemAsync({ id }: DownloadClientItem): Promise<void> {
|
public async resumeItemAsync({ id }: DownloadClientItem): Promise<void> {
|
||||||
await this.sabNzbApiCallAsync("queue", new URLSearchParams({ name: "resume", value: id }));
|
await this.sabNzbApiCallAsync("queue", { name: "resume", value: id });
|
||||||
}
|
}
|
||||||
|
|
||||||
//Delete files prevented on completed files. https://github.com/sabnzbd/sabnzbd/issues/2754
|
//Delete files prevented on completed files. https://github.com/sabnzbd/sabnzbd/issues/2754
|
||||||
//Works on all other in downloading and post-processing.
|
//Works on all other in downloading and post-processing.
|
||||||
//Will stop working as soon as the finished files is moved to completed folder.
|
//Will stop working as soon as the finished files is moved to completed folder.
|
||||||
public async deleteItemAsync({ id, progress }: DownloadClientItem, fromDisk: boolean): Promise<void> {
|
public async deleteItemAsync({ id, progress }: DownloadClientItem, fromDisk: boolean): Promise<void> {
|
||||||
await this.sabNzbApiCallAsync(
|
await this.sabNzbApiCallAsync(progress !== 1 ? "queue" : "history", {
|
||||||
progress !== 1 ? "queue" : "history",
|
name: "delete",
|
||||||
new URLSearchParams({
|
archive: fromDisk ? "0" : "1",
|
||||||
name: "delete",
|
value: id,
|
||||||
archive: fromDisk ? "0" : "1",
|
del_files: fromDisk ? "1" : "0",
|
||||||
value: id,
|
});
|
||||||
del_files: fromDisk ? "1" : "0",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sabNzbApiCallAsync(mode: string, searchParams?: URLSearchParams): Promise<unknown> {
|
private async sabNzbApiCallAsync(mode: string, searchParams?: Record<string, string>): Promise<unknown> {
|
||||||
const url = new URL("api", this.integration.url);
|
const url = this.url("/api", {
|
||||||
url.searchParams.append("output", "json");
|
...searchParams,
|
||||||
url.searchParams.append("mode", mode);
|
output: "json",
|
||||||
searchParams?.forEach((value, key) => {
|
mode,
|
||||||
url.searchParams.append(key, value);
|
apikey: this.getSecretValue("apiKey"),
|
||||||
});
|
});
|
||||||
url.searchParams.append("apikey", this.getSecretValue("apiKey"));
|
|
||||||
return await fetch(url)
|
return await fetch(url)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -71,9 +71,8 @@ export class TransmissionIntegration extends DownloadClientIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getClient() {
|
private getClient() {
|
||||||
const baseUrl = new URL(this.integration.url).href;
|
|
||||||
return new Transmission({
|
return new Transmission({
|
||||||
baseUrl,
|
baseUrl: this.url("/").toString(),
|
||||||
username: this.getSecretValue("username"),
|
username: this.getSecretValue("username"),
|
||||||
password: this.getSecretValue("password"),
|
password: this.getSecretValue("password"),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { appendPath } from "@homarr/common";
|
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
@@ -17,7 +16,7 @@ export class HomeAssistantIntegration extends Integration {
|
|||||||
}
|
}
|
||||||
return entityStateSchema.safeParseAsync(body);
|
return entityStateSchema.safeParseAsync(body);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to fetch from ${this.integration.url}: ${err as string}`);
|
logger.error(`Failed to fetch from ${this.url("/")}: ${err as string}`);
|
||||||
return {
|
return {
|
||||||
success: false as const,
|
success: false as const,
|
||||||
error: err,
|
error: err,
|
||||||
@@ -33,7 +32,7 @@ export class HomeAssistantIntegration extends Integration {
|
|||||||
|
|
||||||
return response.ok;
|
return response.ok;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to fetch from '${this.integration.url}': ${err as string}`);
|
logger.error(`Failed to fetch from '${this.url("/")}': ${err as string}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +51,7 @@ export class HomeAssistantIntegration extends Integration {
|
|||||||
|
|
||||||
return response.ok;
|
return response.ok;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to fetch from '${this.integration.url}': ${err as string}`);
|
logger.error(`Failed to fetch from '${this.url("/")}': ${err as string}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,7 +71,7 @@ export class HomeAssistantIntegration extends Integration {
|
|||||||
* @returns the response from the API
|
* @returns the response from the API
|
||||||
*/
|
*/
|
||||||
private async getAsync(path: `/api/${string}`) {
|
private async getAsync(path: `/api/${string}`) {
|
||||||
return await fetch(appendPath(this.integration.url, path), {
|
return await fetch(this.url(path), {
|
||||||
headers: this.getAuthHeaders(),
|
headers: this.getAuthHeaders(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -85,7 +84,7 @@ export class HomeAssistantIntegration extends Integration {
|
|||||||
* @returns the response from the API
|
* @returns the response from the API
|
||||||
*/
|
*/
|
||||||
private async postAsync(path: `/api/${string}`, body: Record<string, string>) {
|
private async postAsync(path: `/api/${string}`, body: Record<string, string>) {
|
||||||
return await fetch(appendPath(this.integration.url, path), {
|
return await fetch(this.url(path), {
|
||||||
headers: this.getAuthHeaders(),
|
headers: this.getAuthHeaders(),
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -29,9 +29,7 @@ export class JellyfinIntegration extends Integration {
|
|||||||
const sessions = await sessionApi.getSessions();
|
const sessions = await sessionApi.getSessions();
|
||||||
|
|
||||||
if (sessions.status !== 200) {
|
if (sessions.status !== 200) {
|
||||||
throw new Error(
|
throw new Error(`Jellyfin server ${this.url("/")} returned a non successful status code: ${sessions.status}`);
|
||||||
`Jellyfin server ${this.integration.url} returned a non successful status code: ${sessions.status}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sessions.data.map((sessionInfo): StreamSession => {
|
return sessions.data.map((sessionInfo): StreamSession => {
|
||||||
@@ -52,7 +50,7 @@ export class JellyfinIntegration extends Integration {
|
|||||||
sessionId: `${sessionInfo.Id}`,
|
sessionId: `${sessionInfo.Id}`,
|
||||||
sessionName: `${sessionInfo.Client} (${sessionInfo.DeviceName})`,
|
sessionName: `${sessionInfo.Client} (${sessionInfo.DeviceName})`,
|
||||||
user: {
|
user: {
|
||||||
profilePictureUrl: `${this.integration.url}/Users/${sessionInfo.UserId}/Images/Primary`,
|
profilePictureUrl: this.url(`/Users/${sessionInfo.UserId}/Images/Primary`).toString(),
|
||||||
userId: sessionInfo.UserId ?? "",
|
userId: sessionInfo.UserId ?? "",
|
||||||
username: sessionInfo.UserName ?? "",
|
username: sessionInfo.UserName ?? "",
|
||||||
},
|
},
|
||||||
@@ -63,6 +61,6 @@ export class JellyfinIntegration extends Integration {
|
|||||||
|
|
||||||
private getApi() {
|
private getApi() {
|
||||||
const apiKey = this.getSecretValue("apiKey");
|
const apiKey = this.getSecretValue("apiKey");
|
||||||
return this.jellyfin.createApi(this.integration.url, apiKey);
|
return this.jellyfin.createApi(this.url("/").toString(), apiKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export class LidarrIntegration extends MediaOrganizerIntegration {
|
|||||||
public async testConnectionAsync(): Promise<void> {
|
public async testConnectionAsync(): Promise<void> {
|
||||||
await super.handleTestConnectionResponseAsync({
|
await super.handleTestConnectionResponseAsync({
|
||||||
queryFunctionAsync: async () => {
|
queryFunctionAsync: async () => {
|
||||||
return await fetch(`${this.integration.url}/api`, {
|
return await fetch(this.url("/api"), {
|
||||||
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -22,11 +22,12 @@ export class LidarrIntegration extends MediaOrganizerIntegration {
|
|||||||
* @param includeUnmonitored When true results will include unmonitored items of the Tadarr library.
|
* @param includeUnmonitored When true results will include unmonitored items of the Tadarr library.
|
||||||
*/
|
*/
|
||||||
async getCalendarEventsAsync(start: Date, end: Date, includeUnmonitored = true): Promise<CalendarEvent[]> {
|
async getCalendarEventsAsync(start: Date, end: Date, includeUnmonitored = true): Promise<CalendarEvent[]> {
|
||||||
const url = new URL(this.integration.url);
|
const url = this.url("/api/v1/calendar", {
|
||||||
url.pathname = "/api/v1/calendar";
|
start,
|
||||||
url.searchParams.append("start", start.toISOString());
|
end,
|
||||||
url.searchParams.append("end", end.toISOString());
|
unmonitored: includeUnmonitored,
|
||||||
url.searchParams.append("unmonitored", includeUnmonitored ? "true" : "false");
|
});
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": super.getSecretValue("apiKey"),
|
"X-Api-Key": super.getSecretValue("apiKey"),
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ export class RadarrIntegration extends MediaOrganizerIntegration {
|
|||||||
* @param includeUnmonitored When true results will include unmonitored items of the Tadarr library.
|
* @param includeUnmonitored When true results will include unmonitored items of the Tadarr library.
|
||||||
*/
|
*/
|
||||||
async getCalendarEventsAsync(start: Date, end: Date, includeUnmonitored = true): Promise<CalendarEvent[]> {
|
async getCalendarEventsAsync(start: Date, end: Date, includeUnmonitored = true): Promise<CalendarEvent[]> {
|
||||||
const url = new URL(this.integration.url);
|
const url = this.url("/api/v3/calendar", {
|
||||||
url.pathname = "/api/v3/calendar";
|
start,
|
||||||
url.searchParams.append("start", start.toISOString());
|
end,
|
||||||
url.searchParams.append("end", end.toISOString());
|
unmonitored: includeUnmonitored,
|
||||||
url.searchParams.append("unmonitored", includeUnmonitored ? "true" : "false");
|
});
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": super.getSecretValue("apiKey"),
|
"X-Api-Key": super.getSecretValue("apiKey"),
|
||||||
@@ -48,7 +49,7 @@ export class RadarrIntegration extends MediaOrganizerIntegration {
|
|||||||
private getLinksForRadarrCalendarEvent = (event: z.infer<typeof radarrCalendarEventSchema>) => {
|
private getLinksForRadarrCalendarEvent = (event: z.infer<typeof radarrCalendarEventSchema>) => {
|
||||||
const links: CalendarEvent["links"] = [
|
const links: CalendarEvent["links"] = [
|
||||||
{
|
{
|
||||||
href: `${this.integration.url}/movie/${event.titleSlug}`,
|
href: this.url(`/movie/${event.titleSlug}`).toString(),
|
||||||
name: "Radarr",
|
name: "Radarr",
|
||||||
logo: "/images/apps/radarr.svg",
|
logo: "/images/apps/radarr.svg",
|
||||||
color: undefined,
|
color: undefined,
|
||||||
@@ -93,7 +94,7 @@ export class RadarrIntegration extends MediaOrganizerIntegration {
|
|||||||
public async testConnectionAsync(): Promise<void> {
|
public async testConnectionAsync(): Promise<void> {
|
||||||
await super.handleTestConnectionResponseAsync({
|
await super.handleTestConnectionResponseAsync({
|
||||||
queryFunctionAsync: async () => {
|
queryFunctionAsync: async () => {
|
||||||
return await fetch(`${this.integration.url}/api`, {
|
return await fetch(this.url("/api"), {
|
||||||
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export class ReadarrIntegration extends MediaOrganizerIntegration {
|
|||||||
public async testConnectionAsync(): Promise<void> {
|
public async testConnectionAsync(): Promise<void> {
|
||||||
await super.handleTestConnectionResponseAsync({
|
await super.handleTestConnectionResponseAsync({
|
||||||
queryFunctionAsync: async () => {
|
queryFunctionAsync: async () => {
|
||||||
return await fetch(`${this.integration.url}/api`, {
|
return await fetch(this.url("/api"), {
|
||||||
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -27,12 +27,13 @@ export class ReadarrIntegration extends MediaOrganizerIntegration {
|
|||||||
includeUnmonitored = true,
|
includeUnmonitored = true,
|
||||||
includeAuthor = true,
|
includeAuthor = true,
|
||||||
): Promise<CalendarEvent[]> {
|
): Promise<CalendarEvent[]> {
|
||||||
const url = new URL(this.integration.url);
|
const url = this.url("/api/v1/calendar", {
|
||||||
url.pathname = "/api/v1/calendar";
|
start,
|
||||||
url.searchParams.append("start", start.toISOString());
|
end,
|
||||||
url.searchParams.append("end", end.toISOString());
|
unmonitored: includeUnmonitored,
|
||||||
url.searchParams.append("unmonitored", includeUnmonitored.toString());
|
includeAuthor,
|
||||||
url.searchParams.append("includeAuthor", includeAuthor.toString());
|
});
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": super.getSecretValue("apiKey"),
|
"X-Api-Key": super.getSecretValue("apiKey"),
|
||||||
@@ -58,7 +59,7 @@ export class ReadarrIntegration extends MediaOrganizerIntegration {
|
|||||||
private getLinksForReadarrCalendarEvent = (event: z.infer<typeof readarrCalendarEventSchema>) => {
|
private getLinksForReadarrCalendarEvent = (event: z.infer<typeof readarrCalendarEventSchema>) => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
href: `${this.integration.url}/author/${event.author.foreignAuthorId}`,
|
href: this.url(`/author/${event.author.foreignAuthorId}`).toString(),
|
||||||
color: "#f5c518",
|
color: "#f5c518",
|
||||||
isDark: false,
|
isDark: false,
|
||||||
logo: "/images/apps/readarr.svg",
|
logo: "/images/apps/readarr.svg",
|
||||||
@@ -85,7 +86,7 @@ export class ReadarrIntegration extends MediaOrganizerIntegration {
|
|||||||
if (!bestImage) {
|
if (!bestImage) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return `${this.integration.url}${bestImage.url}`;
|
return this.url(bestImage.url as `/${string}`).toString();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,15 @@ export class SonarrIntegration extends MediaOrganizerIntegration {
|
|||||||
* @param includeUnmonitored When true results will include unmonitored items of the Sonarr library.
|
* @param includeUnmonitored When true results will include unmonitored items of the Sonarr library.
|
||||||
*/
|
*/
|
||||||
async getCalendarEventsAsync(start: Date, end: Date, includeUnmonitored = true): Promise<CalendarEvent[]> {
|
async getCalendarEventsAsync(start: Date, end: Date, includeUnmonitored = true): Promise<CalendarEvent[]> {
|
||||||
const url = new URL(this.integration.url);
|
const url = this.url("/api/v3/calendar", {
|
||||||
url.pathname = "/api/v3/calendar";
|
start,
|
||||||
url.searchParams.append("start", start.toISOString());
|
end,
|
||||||
url.searchParams.append("end", end.toISOString());
|
unmonitored: includeUnmonitored,
|
||||||
url.searchParams.append("includeSeries", "true");
|
includeSeries: true,
|
||||||
url.searchParams.append("includeEpisodeFile", "true");
|
includeEpisodeFile: true,
|
||||||
url.searchParams.append("includeEpisodeImages", "true");
|
includeEpisodeImages: true,
|
||||||
url.searchParams.append("unmonitored", includeUnmonitored ? "true" : "false");
|
});
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": super.getSecretValue("apiKey"),
|
"X-Api-Key": super.getSecretValue("apiKey"),
|
||||||
@@ -47,7 +48,7 @@ export class SonarrIntegration extends MediaOrganizerIntegration {
|
|||||||
private getLinksForSonarCalendarEvent = (event: z.infer<typeof sonarrCalendarEventSchema>) => {
|
private getLinksForSonarCalendarEvent = (event: z.infer<typeof sonarrCalendarEventSchema>) => {
|
||||||
const links: CalendarEvent["links"] = [
|
const links: CalendarEvent["links"] = [
|
||||||
{
|
{
|
||||||
href: `${this.integration.url}/series/${event.series.titleSlug}`,
|
href: this.url(`/series/${event.series.titleSlug}`).toString(),
|
||||||
name: "Sonarr",
|
name: "Sonarr",
|
||||||
logo: "/images/apps/sonarr.svg",
|
logo: "/images/apps/sonarr.svg",
|
||||||
color: undefined,
|
color: undefined,
|
||||||
@@ -92,7 +93,7 @@ export class SonarrIntegration extends MediaOrganizerIntegration {
|
|||||||
public async testConnectionAsync(): Promise<void> {
|
public async testConnectionAsync(): Promise<void> {
|
||||||
await super.handleTestConnectionResponseAsync({
|
await super.handleTestConnectionResponseAsync({
|
||||||
queryFunctionAsync: async () => {
|
queryFunctionAsync: async () => {
|
||||||
return await fetch(`${this.integration.url}/api`, {
|
return await fetch(this.url("/api"), {
|
||||||
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export class OpenMediaVaultIntegration extends Integration {
|
|||||||
params: Record<string, unknown>,
|
params: Record<string, unknown>,
|
||||||
headers: Record<string, string> = {},
|
headers: Record<string, string> = {},
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
return await fetch(`${this.integration.url}/rpc.php`, {
|
return await fetch(this.url("/rpc.php"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { MediaAvailability, MediaRequestStatus } from "../interfaces/media-reque
|
|||||||
*/
|
*/
|
||||||
export class OverseerrIntegration extends Integration implements ISearchableIntegration {
|
export class OverseerrIntegration extends Integration implements ISearchableIntegration {
|
||||||
public async searchAsync(query: string): Promise<{ image?: string; name: string; link: string; text?: string }[]> {
|
public async searchAsync(query: string): Promise<{ image?: string; name: string; link: string; text?: string }[]> {
|
||||||
const response = await fetch(`${this.integration.url}/api/v1/search?query=${query}`, {
|
const response = await fetch(this.url("/api/v1/search", { query }), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||||
},
|
},
|
||||||
@@ -24,13 +24,14 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
|
|
||||||
return schemaData.results.map((result) => ({
|
return schemaData.results.map((result) => ({
|
||||||
name: "name" in result ? result.name : result.title,
|
name: "name" in result ? result.name : result.title,
|
||||||
link: `${this.integration.url}/${result.mediaType}/${result.id}`,
|
link: this.url(`/${result.mediaType}/${result.id}`).toString(),
|
||||||
image: constructSearchResultImage(this.integration.url, result),
|
image: constructSearchResultImage(result),
|
||||||
text: "overview" in result ? result.overview : undefined,
|
text: "overview" in result ? result.overview : undefined,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async testConnectionAsync(): Promise<void> {
|
public async testConnectionAsync(): Promise<void> {
|
||||||
const response = await fetch(`${this.integration.url}/api/v1/auth/me`, {
|
const response = await fetch(this.url("/api/v1/auth/me"), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||||
},
|
},
|
||||||
@@ -46,14 +47,14 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
|
|
||||||
public async getRequestsAsync(): Promise<MediaRequest[]> {
|
public async getRequestsAsync(): Promise<MediaRequest[]> {
|
||||||
//Ensure to get all pending request first
|
//Ensure to get all pending request first
|
||||||
const pendingRequests = await fetch(`${this.integration.url}/api/v1/request?take=-1&filter=pending`, {
|
const pendingRequests = await fetch(this.url("/api/v1/request", { take: -1, filter: "pending" }), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
//Change 20 to integration setting (set to -1 for all)
|
//Change 20 to integration setting (set to -1 for all)
|
||||||
const allRequests = await fetch(`${this.integration.url}/api/v1/request?take=20`, {
|
const allRequests = await fetch(this.url("/api/v1/request", { take: 20 }), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||||
},
|
},
|
||||||
@@ -83,7 +84,7 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
availability: request.media.status,
|
availability: request.media.status,
|
||||||
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.integration.url}/${request.type}/${request.media.tmdbId}`,
|
href: this.url(`/${request.type}/${request.media.tmdbId}`).toString(),
|
||||||
type: request.type,
|
type: request.type,
|
||||||
createdAt: request.createdAt,
|
createdAt: request.createdAt,
|
||||||
airDate: new Date(information.airDate),
|
airDate: new Date(information.airDate),
|
||||||
@@ -91,8 +92,8 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
? ({
|
? ({
|
||||||
...request.requestedBy,
|
...request.requestedBy,
|
||||||
displayName: request.requestedBy.displayName,
|
displayName: request.requestedBy.displayName,
|
||||||
link: `${this.integration.url}/users/${request.requestedBy.id}`,
|
link: this.url(`/users/${request.requestedBy.id}`).toString(),
|
||||||
avatar: constructAvatarUrl(this.integration.url, request.requestedBy.avatar),
|
avatar: this.constructAvatarUrl(request.requestedBy.avatar).toString(),
|
||||||
} satisfies Omit<RequestUser, "requestCount">)
|
} satisfies Omit<RequestUser, "requestCount">)
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
@@ -101,7 +102,7 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getStatsAsync(): Promise<RequestStats> {
|
public async getStatsAsync(): Promise<RequestStats> {
|
||||||
const response = await fetch(`${this.integration.url}/api/v1/request/count`, {
|
const response = await fetch(this.url("/api/v1/request/count"), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||||
},
|
},
|
||||||
@@ -110,7 +111,7 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getUsersAsync(): Promise<RequestUser[]> {
|
public async getUsersAsync(): Promise<RequestUser[]> {
|
||||||
const response = await fetch(`${this.integration.url}/api/v1/user?take=-1`, {
|
const response = await fetch(this.url("/api/v1/user", { take: -1 }), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||||
},
|
},
|
||||||
@@ -119,15 +120,15 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
return users.map((user): RequestUser => {
|
return users.map((user): RequestUser => {
|
||||||
return {
|
return {
|
||||||
...user,
|
...user,
|
||||||
link: `${this.integration.url}/users/${user.id}`,
|
link: this.url(`/users/${user.id}`).toString(),
|
||||||
avatar: constructAvatarUrl(this.integration.url, user.avatar),
|
avatar: this.constructAvatarUrl(user.avatar).toString(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async approveRequestAsync(requestId: number): Promise<void> {
|
public async approveRequestAsync(requestId: number): Promise<void> {
|
||||||
logger.info(`Approving media request id='${requestId}' integration='${this.integration.name}'`);
|
logger.info(`Approving media request id='${requestId}' integration='${this.integration.name}'`);
|
||||||
await fetch(`${this.integration.url}/api/v1/request/${requestId}/approve`, {
|
await fetch(this.url(`/api/v1/request/${requestId}/approve`), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||||
@@ -145,7 +146,7 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
|
|
||||||
public async declineRequestAsync(requestId: number): Promise<void> {
|
public async declineRequestAsync(requestId: number): Promise<void> {
|
||||||
logger.info(`Declining media request id='${requestId}' integration='${this.integration.name}'`);
|
logger.info(`Declining media request id='${requestId}' integration='${this.integration.name}'`);
|
||||||
await fetch(`${this.integration.url}/api/v1/request/${requestId}/decline`, {
|
await fetch(this.url(`/api/v1/request/${requestId}/decline`), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||||
@@ -162,7 +163,7 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getItemInformationAsync(id: number, type: MediaRequest["type"]): Promise<MediaInformation> {
|
private async getItemInformationAsync(id: number, type: MediaRequest["type"]): Promise<MediaInformation> {
|
||||||
const response = await fetch(`${this.integration.url}/api/v1/${type}/${id}`, {
|
const response = await fetch(this.url(`/api/v1/${type}/${id}`), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||||
},
|
},
|
||||||
@@ -186,17 +187,17 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
|
|||||||
airDate: movie.releaseDate,
|
airDate: movie.releaseDate,
|
||||||
} satisfies MediaInformation;
|
} satisfies MediaInformation;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const constructAvatarUrl = (appUrl: string, avatar: string) => {
|
private constructAvatarUrl(avatar: string) {
|
||||||
const isAbsolute = avatar.startsWith("http://") || avatar.startsWith("https://");
|
const isAbsolute = avatar.startsWith("http://") || avatar.startsWith("https://");
|
||||||
|
|
||||||
if (isAbsolute) {
|
if (isAbsolute) {
|
||||||
return avatar;
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.url(`/${avatar}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return `${appUrl}/${avatar}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface MediaInformation {
|
interface MediaInformation {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -308,11 +309,8 @@ const getUsersSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const constructSearchResultImage = (
|
const constructSearchResultImage = (result: Exclude<z.infer<typeof searchSchema>["results"], undefined>[number]) => {
|
||||||
appUrl: string,
|
const path = getResultImagePath(result);
|
||||||
result: Exclude<z.infer<typeof searchSchema>["results"], undefined>[number],
|
|
||||||
) => {
|
|
||||||
const path = getResultImagePath(appUrl, result);
|
|
||||||
if (!path) {
|
if (!path) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -320,10 +318,7 @@ const constructSearchResultImage = (
|
|||||||
return `https://image.tmdb.org/t/p/w600_and_h900_bestv2${path}`;
|
return `https://image.tmdb.org/t/p/w600_and_h900_bestv2${path}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getResultImagePath = (
|
const getResultImagePath = (result: Exclude<z.infer<typeof searchSchema>["results"], undefined>[number]) => {
|
||||||
appUrl: string,
|
|
||||||
result: Exclude<z.infer<typeof searchSchema>["results"], undefined>[number],
|
|
||||||
) => {
|
|
||||||
switch (result.mediaType) {
|
switch (result.mediaType) {
|
||||||
case "person":
|
case "person":
|
||||||
return result.profilePath;
|
return result.profilePath;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { summaryResponseSchema } from "./pi-hole-types";
|
|||||||
export class PiHoleIntegration extends Integration implements DnsHoleSummaryIntegration {
|
export class PiHoleIntegration extends Integration implements DnsHoleSummaryIntegration {
|
||||||
public async getSummaryAsync(): Promise<DnsHoleSummary> {
|
public async getSummaryAsync(): Promise<DnsHoleSummary> {
|
||||||
const apiKey = super.getSecretValue("apiKey");
|
const apiKey = super.getSecretValue("apiKey");
|
||||||
const response = await fetch(`${this.integration.url}/admin/api.php?summaryRaw&auth=${apiKey}`);
|
const response = await fetch(this.url("/admin/api.php?summaryRaw", { auth: apiKey }));
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to fetch summary for ${this.integration.name} (${this.integration.id}): ${response.statusText}`,
|
`Failed to fetch summary for ${this.integration.name} (${this.integration.id}): ${response.statusText}`,
|
||||||
@@ -36,7 +36,7 @@ export class PiHoleIntegration extends Integration implements DnsHoleSummaryInte
|
|||||||
|
|
||||||
await super.handleTestConnectionResponseAsync({
|
await super.handleTestConnectionResponseAsync({
|
||||||
queryFunctionAsync: async () => {
|
queryFunctionAsync: async () => {
|
||||||
return await fetch(`${this.integration.url}/admin/api.php?status&auth=${apiKey}`);
|
return await fetch(this.url("/admin/api.php?status", { auth: apiKey }));
|
||||||
},
|
},
|
||||||
handleResponseAsync: async (response) => {
|
handleResponseAsync: async (response) => {
|
||||||
try {
|
try {
|
||||||
@@ -53,7 +53,7 @@ export class PiHoleIntegration extends Integration implements DnsHoleSummaryInte
|
|||||||
|
|
||||||
public async enableAsync(): Promise<void> {
|
public async enableAsync(): Promise<void> {
|
||||||
const apiKey = super.getSecretValue("apiKey");
|
const apiKey = super.getSecretValue("apiKey");
|
||||||
const response = await fetch(`${this.integration.url}/admin/api.php?enable&auth=${apiKey}`);
|
const response = await fetch(this.url("/admin/api.php?enable", { auth: apiKey }));
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to enable PiHole for ${this.integration.name} (${this.integration.id}): ${response.statusText}`,
|
`Failed to enable PiHole for ${this.integration.name} (${this.integration.id}): ${response.statusText}`,
|
||||||
@@ -63,7 +63,7 @@ export class PiHoleIntegration extends Integration implements DnsHoleSummaryInte
|
|||||||
|
|
||||||
public async disableAsync(duration?: number): Promise<void> {
|
public async disableAsync(duration?: number): Promise<void> {
|
||||||
const apiKey = super.getSecretValue("apiKey");
|
const apiKey = super.getSecretValue("apiKey");
|
||||||
const url = `${this.integration.url}/admin/api.php?disable${duration ? `=${duration}` : ""}&auth=${apiKey}`;
|
const url = this.url(`/admin/api.php?disable${duration ? `=${duration}` : ""}`, { auth: apiKey });
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export class PlexIntegration extends Integration {
|
|||||||
public async getCurrentSessionsAsync(): Promise<StreamSession[]> {
|
public async getCurrentSessionsAsync(): Promise<StreamSession[]> {
|
||||||
const token = super.getSecretValue("apiKey");
|
const token = super.getSecretValue("apiKey");
|
||||||
|
|
||||||
const response = await fetch(`${this.integration.url}/status/sessions`, {
|
const response = await fetch(this.url("/status/sessions"), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Plex-Token": token,
|
"X-Plex-Token": token,
|
||||||
},
|
},
|
||||||
@@ -66,7 +66,7 @@ export class PlexIntegration extends Integration {
|
|||||||
|
|
||||||
await super.handleTestConnectionResponseAsync({
|
await super.handleTestConnectionResponseAsync({
|
||||||
queryFunctionAsync: async () => {
|
queryFunctionAsync: async () => {
|
||||||
return await fetch(this.integration.url, {
|
return await fetch(this.url("/"), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Plex-Token": token,
|
"X-Plex-Token": token,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export class ProwlarrIntegration extends Integration {
|
|||||||
public async getIndexersAsync(): Promise<Indexer[]> {
|
public async getIndexersAsync(): Promise<Indexer[]> {
|
||||||
const apiKey = super.getSecretValue("apiKey");
|
const apiKey = super.getSecretValue("apiKey");
|
||||||
|
|
||||||
const indexerResponse = await fetch(`${this.integration.url}/api/v1/indexer`, {
|
const indexerResponse = await fetch(this.url("/api/v1/indexer"), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": apiKey,
|
"X-Api-Key": apiKey,
|
||||||
},
|
},
|
||||||
@@ -18,7 +18,7 @@ export class ProwlarrIntegration extends Integration {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusResponse = await fetch(`${this.integration.url}/api/v1/indexerstatus`, {
|
const statusResponse = await fetch(this.url("/api/v1/indexerstatus"), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": apiKey,
|
"X-Api-Key": apiKey,
|
||||||
},
|
},
|
||||||
@@ -60,7 +60,7 @@ export class ProwlarrIntegration extends Integration {
|
|||||||
|
|
||||||
public async testAllAsync(): Promise<void> {
|
public async testAllAsync(): Promise<void> {
|
||||||
const apiKey = super.getSecretValue("apiKey");
|
const apiKey = super.getSecretValue("apiKey");
|
||||||
const response = await fetch(`${this.integration.url}/api/v1/indexer/testall`, {
|
const response = await fetch(this.url("/api/v1/indexer/testall"), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": apiKey,
|
"X-Api-Key": apiKey,
|
||||||
},
|
},
|
||||||
@@ -78,7 +78,7 @@ export class ProwlarrIntegration extends Integration {
|
|||||||
|
|
||||||
await super.handleTestConnectionResponseAsync({
|
await super.handleTestConnectionResponseAsync({
|
||||||
queryFunctionAsync: async () => {
|
queryFunctionAsync: async () => {
|
||||||
return await fetch(`${this.integration.url}/api`, {
|
return await fetch(this.url("/api"), {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Api-Key": apiKey,
|
"X-Api-Key": apiKey,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,14 +27,14 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ioredis": "5.4.1",
|
"ioredis": "5.4.1",
|
||||||
"superjson": "2.2.1",
|
"superjson": "2.2.2",
|
||||||
"winston": "3.17.0"
|
"winston": "3.17.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@homarr/modals-collection",
|
"name": "@homarr/modals-collection",
|
||||||
"private": true,
|
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.ts"
|
".": "./index.ts"
|
||||||
@@ -13,13 +14,13 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -rf .turbo node_modules",
|
"clean": "rm -rf .turbo node_modules",
|
||||||
"lint": "eslint",
|
|
||||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
|
"lint": "eslint",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/api": "workspace:^0.1.0",
|
"@homarr/api": "workspace:^0.1.0",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
@@ -31,17 +32,16 @@
|
|||||||
"@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": "^7.14.3",
|
"@mantine/core": "^7.14.3",
|
||||||
"@tabler/icons-react": "^3.23.0",
|
"@tabler/icons-react": "^3.24.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"next": "^14.2.18",
|
"next": "^14.2.20",
|
||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
},
|
}
|
||||||
"prettier": "@homarr/prettier-config"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@mantine/notifications": "^7.14.3",
|
"@mantine/notifications": "^7.14.3",
|
||||||
"@tabler/icons-react": "^3.23.0"
|
"@tabler/icons-react": "^3.24.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@homarr/old-import",
|
"name": "@homarr/old-import",
|
||||||
"private": true,
|
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.ts"
|
".": "./index.ts"
|
||||||
@@ -13,13 +14,13 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -rf .turbo node_modules",
|
"clean": "rm -rf .turbo node_modules",
|
||||||
"lint": "eslint",
|
|
||||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
|
"lint": "eslint",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
@@ -27,14 +28,13 @@
|
|||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@homarr/old-schema": "workspace:^0.1.0",
|
"@homarr/old-schema": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"superjson": "2.2.1"
|
"superjson": "2.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
},
|
}
|
||||||
"prettier": "@homarr/prettier-config"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@homarr/old-schema",
|
"name": "@homarr/old-schema",
|
||||||
"private": true,
|
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.ts"
|
".": "./index.ts"
|
||||||
@@ -13,13 +14,13 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -rf .turbo node_modules",
|
"clean": "rm -rf .turbo node_modules",
|
||||||
"lint": "eslint",
|
|
||||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
|
"lint": "eslint",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
@@ -27,8 +28,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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
},
|
}
|
||||||
"prettier": "@homarr/prettier-config"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,13 +27,13 @@
|
|||||||
"@homarr/definitions": "workspace:^",
|
"@homarr/definitions": "workspace:^",
|
||||||
"@homarr/log": "workspace:^",
|
"@homarr/log": "workspace:^",
|
||||||
"ioredis": "5.4.1",
|
"ioredis": "5.4.1",
|
||||||
"superjson": "2.2.1"
|
"superjson": "2.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@homarr/request-handler",
|
"name": "@homarr/request-handler",
|
||||||
"private": true,
|
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
"./*": "./src/*.ts"
|
"./*": "./src/*.ts"
|
||||||
@@ -13,13 +14,13 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -rf .turbo node_modules",
|
"clean": "rm -rf .turbo node_modules",
|
||||||
"lint": "eslint",
|
|
||||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
|
"lint": "eslint",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
@@ -28,14 +29,14 @@
|
|||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"superjson": "2.2.1"
|
"pretty-print-error": "^1.1.2",
|
||||||
|
"superjson": "2.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
},
|
}
|
||||||
"prettier": "@homarr/prettier-config"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { formatError } from "pretty-print-error";
|
||||||
import SuperJSON from "superjson";
|
import SuperJSON from "superjson";
|
||||||
|
|
||||||
import { hashObjectBase64, Stopwatch } from "@homarr/common";
|
import { hashObjectBase64, Stopwatch } from "@homarr/common";
|
||||||
@@ -95,7 +96,7 @@ export const createRequestIntegrationJobHandler = <
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to run integration job integration=${integrationId} inputHash='${inputHash}' error=${error as string}`,
|
`Failed to run integration job integration=${integrationId} inputHash='${inputHash}' error=${formatError(error)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/api": "workspace:^0.1.0",
|
"@homarr/api": "workspace:^0.1.0",
|
||||||
"@homarr/auth": "workspace:^0.1.0",
|
"@homarr/auth": "workspace:^0.1.0",
|
||||||
@@ -34,9 +35,9 @@
|
|||||||
"@mantine/core": "^7.14.3",
|
"@mantine/core": "^7.14.3",
|
||||||
"@mantine/hooks": "^7.14.3",
|
"@mantine/hooks": "^7.14.3",
|
||||||
"@mantine/spotlight": "^7.14.3",
|
"@mantine/spotlight": "^7.14.3",
|
||||||
"@tabler/icons-react": "^3.23.0",
|
"@tabler/icons-react": "^3.24.0",
|
||||||
"jotai": "^2.10.3",
|
"jotai": "^2.10.3",
|
||||||
"next": "^14.2.18",
|
"next": "^14.2.20",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"use-deep-compare-effect": "^1.8.1"
|
"use-deep-compare-effect": "^1.8.1"
|
||||||
},
|
},
|
||||||
@@ -44,8 +45,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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
},
|
}
|
||||||
"prettier": "@homarr/prettier-config"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ export const SpotlightGroupActions = <TOption extends Record<string, unknown>>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (Array.isArray(options)) {
|
if (Array.isArray(options)) {
|
||||||
|
if (options.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const filteredOptions = options
|
const filteredOptions = options
|
||||||
.filter((option) => ("filter" in group ? group.filter(query, option) : false))
|
.filter((option) => ("filter" in group ? group.filter(query, option) : false))
|
||||||
.sort((optionA, optionB) => {
|
.sort((optionA, optionB) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import { useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { ActionIcon, Center, Group, Kbd } from "@mantine/core";
|
import { ActionIcon, Center, Group, Kbd } from "@mantine/core";
|
||||||
import { Spotlight as MantineSpotlight } from "@mantine/spotlight";
|
import { Spotlight as MantineSpotlight } from "@mantine/spotlight";
|
||||||
import { IconSearch, IconX } from "@tabler/icons-react";
|
import { IconSearch, IconX } from "@tabler/icons-react";
|
||||||
@@ -12,6 +12,7 @@ import { useI18n } from "@homarr/translation/client";
|
|||||||
import type { inferSearchInteractionOptions } from "../lib/interaction";
|
import type { inferSearchInteractionOptions } from "../lib/interaction";
|
||||||
import type { SearchMode } from "../lib/mode";
|
import type { SearchMode } from "../lib/mode";
|
||||||
import { searchModes } from "../modes";
|
import { searchModes } from "../modes";
|
||||||
|
import { useSpotlightContextResults } from "../modes/home/context";
|
||||||
import { selectAction, spotlightStore } from "../spotlight-store";
|
import { selectAction, spotlightStore } from "../spotlight-store";
|
||||||
import { SpotlightChildrenActions } from "./actions/children-actions";
|
import { SpotlightChildrenActions } from "./actions/children-actions";
|
||||||
import { SpotlightActionGroups } from "./actions/groups/action-group";
|
import { SpotlightActionGroups } from "./actions/groups/action-group";
|
||||||
@@ -19,24 +20,45 @@ import { SpotlightActionGroups } from "./actions/groups/action-group";
|
|||||||
type SearchModeKey = keyof TranslationObject["search"]["mode"];
|
type SearchModeKey = keyof TranslationObject["search"]["mode"];
|
||||||
|
|
||||||
export const Spotlight = () => {
|
export const Spotlight = () => {
|
||||||
const searchModeState = useState<SearchModeKey>("help");
|
const items = useSpotlightContextResults();
|
||||||
|
// We fallback to help if no context results are available
|
||||||
|
const defaultMode = items.length >= 1 ? "home" : "help";
|
||||||
|
const searchModeState = useState<SearchModeKey>(defaultMode);
|
||||||
const mode = searchModeState[0];
|
const mode = searchModeState[0];
|
||||||
const activeMode = useMemo(() => searchModes.find((searchMode) => searchMode.modeKey === mode), [mode]);
|
const activeMode = useMemo(() => searchModes.find((searchMode) => searchMode.modeKey === mode), [mode]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The below logic is used to switch to home page if any context results are registered
|
||||||
|
* or to help page if context results are unregistered
|
||||||
|
*/
|
||||||
|
const previousLengthRef = useRef(items.length);
|
||||||
|
useEffect(() => {
|
||||||
|
if (items.length >= 1 && previousLengthRef.current === 0) {
|
||||||
|
searchModeState[1]("home");
|
||||||
|
} else if (items.length === 0 && previousLengthRef.current >= 1) {
|
||||||
|
searchModeState[1]("help");
|
||||||
|
}
|
||||||
|
|
||||||
|
previousLengthRef.current = items.length;
|
||||||
|
}, [items.length, searchModeState]);
|
||||||
|
|
||||||
if (!activeMode) {
|
if (!activeMode) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We use the "key" below to prevent the 'Different amounts of hooks' error
|
// We use the "key" below to prevent the 'Different amounts of hooks' error
|
||||||
return <SpotlightWithActiveMode key={mode} modeState={searchModeState} activeMode={activeMode} />;
|
return (
|
||||||
|
<SpotlightWithActiveMode key={mode} modeState={searchModeState} activeMode={activeMode} defaultMode={defaultMode} />
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SpotlightWithActiveModeProps {
|
interface SpotlightWithActiveModeProps {
|
||||||
modeState: [SearchModeKey, Dispatch<SetStateAction<SearchModeKey>>];
|
modeState: [SearchModeKey, Dispatch<SetStateAction<SearchModeKey>>];
|
||||||
activeMode: SearchMode;
|
activeMode: SearchMode;
|
||||||
|
defaultMode: SearchModeKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SpotlightWithActiveMode = ({ modeState, activeMode }: SpotlightWithActiveModeProps) => {
|
const SpotlightWithActiveMode = ({ modeState, activeMode, defaultMode }: SpotlightWithActiveModeProps) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [mode, setMode] = modeState;
|
const [mode, setMode] = modeState;
|
||||||
const [childrenOptions, setChildrenOptions] = useState<inferSearchInteractionOptions<"children"> | null>(null);
|
const [childrenOptions, setChildrenOptions] = useState<inferSearchInteractionOptions<"children"> | null>(null);
|
||||||
@@ -50,12 +72,12 @@ const SpotlightWithActiveMode = ({ modeState, activeMode }: SpotlightWithActiveM
|
|||||||
<MantineSpotlight.Root
|
<MantineSpotlight.Root
|
||||||
yOffset={8}
|
yOffset={8}
|
||||||
onSpotlightClose={() => {
|
onSpotlightClose={() => {
|
||||||
setMode("help");
|
setMode(defaultMode);
|
||||||
setChildrenOptions(null);
|
setChildrenOptions(null);
|
||||||
}}
|
}}
|
||||||
query={query}
|
query={query}
|
||||||
onQueryChange={(query) => {
|
onQueryChange={(query) => {
|
||||||
if (mode !== "help" || query.length !== 1) {
|
if ((mode !== "help" && mode !== "home") || query.length !== 1) {
|
||||||
setQuery(query);
|
setQuery(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,13 +95,13 @@ const SpotlightWithActiveMode = ({ modeState, activeMode }: SpotlightWithActiveM
|
|||||||
<MantineSpotlight.Search
|
<MantineSpotlight.Search
|
||||||
placeholder={`${t("search.placeholder")}...`}
|
placeholder={`${t("search.placeholder")}...`}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
leftSectionWidth={activeMode.modeKey !== "help" ? 80 : 48}
|
leftSectionWidth={activeMode.modeKey !== defaultMode ? 80 : 48}
|
||||||
leftSection={
|
leftSection={
|
||||||
<Group align="center" wrap="nowrap" gap="xs" w="100%" h="100%">
|
<Group align="center" wrap="nowrap" gap="xs" w="100%" h="100%">
|
||||||
<Center w={48} h="100%">
|
<Center w={48} h="100%">
|
||||||
<IconSearch stroke={1.5} />
|
<IconSearch stroke={1.5} />
|
||||||
</Center>
|
</Center>
|
||||||
{activeMode.modeKey !== "help" ? <Kbd size="sm">{activeMode.character}</Kbd> : null}
|
{activeMode.modeKey !== defaultMode ? <Kbd size="sm">{activeMode.character}</Kbd> : null}
|
||||||
</Group>
|
</Group>
|
||||||
}
|
}
|
||||||
styles={{
|
styles={{
|
||||||
@@ -88,10 +110,10 @@ const SpotlightWithActiveMode = ({ modeState, activeMode }: SpotlightWithActiveM
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
rightSection={
|
rightSection={
|
||||||
mode === "help" ? undefined : (
|
mode === defaultMode ? undefined : (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMode("help");
|
setMode(defaultMode);
|
||||||
setChildrenOptions(null);
|
setChildrenOptions(null);
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
@@ -103,8 +125,8 @@ const SpotlightWithActiveMode = ({ modeState, activeMode }: SpotlightWithActiveM
|
|||||||
}
|
}
|
||||||
value={query}
|
value={query}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (query.length === 0 && mode !== "help" && event.key === "Backspace") {
|
if (query.length === 0 && mode !== defaultMode && event.key === "Backspace") {
|
||||||
setMode("help");
|
setMode(defaultMode);
|
||||||
setChildrenOptions(null);
|
setChildrenOptions(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -4,5 +4,10 @@ import { spotlightActions } from "./spotlight-store";
|
|||||||
|
|
||||||
export { Spotlight } from "./components/spotlight";
|
export { Spotlight } from "./components/spotlight";
|
||||||
export { openSpotlight };
|
export { openSpotlight };
|
||||||
|
export {
|
||||||
|
SpotlightProvider,
|
||||||
|
useRegisterSpotlightContextResults,
|
||||||
|
useRegisterSpotlightContextActions,
|
||||||
|
} from "./modes/home/context";
|
||||||
|
|
||||||
const openSpotlight = spotlightActions.open;
|
const openSpotlight = spotlightActions.open;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { SearchGroup } from "./group";
|
|||||||
|
|
||||||
export type SearchMode = {
|
export type SearchMode = {
|
||||||
modeKey: keyof TranslationObject["search"]["mode"];
|
modeKey: keyof TranslationObject["search"]["mode"];
|
||||||
character: string;
|
character: string | undefined;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
groups: SearchGroup[];
|
groups: SearchGroup[];
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Group, Text } from "@mantine/core";
|
||||||
|
|
||||||
|
import { createGroup } from "../../lib/group";
|
||||||
|
import type { ContextSpecificItem } from "../home/context";
|
||||||
|
import { useSpotlightContextActions } from "../home/context";
|
||||||
|
|
||||||
|
export const contextSpecificActionsSearchGroups = createGroup<ContextSpecificItem>({
|
||||||
|
title: (t) => t("search.mode.command.group.localCommand.title"),
|
||||||
|
keyPath: "id",
|
||||||
|
Component(option) {
|
||||||
|
const icon =
|
||||||
|
typeof option.icon !== "string" ? (
|
||||||
|
<option.icon size={24} />
|
||||||
|
) : (
|
||||||
|
<img width={24} height={24} src={option.icon} alt={option.name} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group w="100%" wrap="nowrap" align="center" px="md" py="xs">
|
||||||
|
{icon}
|
||||||
|
<Text>{option.name}</Text>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
useInteraction(option) {
|
||||||
|
return option.interaction();
|
||||||
|
},
|
||||||
|
filter(query, option) {
|
||||||
|
return option.name.toLowerCase().includes(query.toLowerCase());
|
||||||
|
},
|
||||||
|
useOptions() {
|
||||||
|
return useSpotlightContextActions();
|
||||||
|
},
|
||||||
|
});
|
||||||
166
packages/spotlight/src/modes/command/global-group.tsx
Normal file
166
packages/spotlight/src/modes/command/global-group.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { Group, Text, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import type { TablerIcon } from "@tabler/icons-react";
|
||||||
|
import {
|
||||||
|
IconBox,
|
||||||
|
IconCategoryPlus,
|
||||||
|
IconFileImport,
|
||||||
|
IconLanguage,
|
||||||
|
IconMailForward,
|
||||||
|
IconMoon,
|
||||||
|
IconPlug,
|
||||||
|
IconSun,
|
||||||
|
IconUserPlus,
|
||||||
|
IconUsersGroup,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
|
import { useSession } from "@homarr/auth/client";
|
||||||
|
import { useModalAction } from "@homarr/modals";
|
||||||
|
import { AddBoardModal, AddGroupModal, ImportBoardModal, InviteCreateModal } from "@homarr/modals-collection";
|
||||||
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
|
import { createGroup } from "../../lib/group";
|
||||||
|
import type { inferSearchInteractionDefinition, SearchInteraction } from "../../lib/interaction";
|
||||||
|
import { interaction } from "../../lib/interaction";
|
||||||
|
import { languageChildrenOptions } from "./children/language";
|
||||||
|
import { newIntegrationChildrenOptions } from "./children/new-integration";
|
||||||
|
|
||||||
|
// This has to be type so it can be interpreted as Record<string, unknown>.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||||
|
type Command<TSearchInteraction extends SearchInteraction = SearchInteraction> = {
|
||||||
|
commandKey: string;
|
||||||
|
icon: TablerIcon;
|
||||||
|
name: string;
|
||||||
|
useInteraction: (
|
||||||
|
_c: Command<TSearchInteraction>,
|
||||||
|
query: string,
|
||||||
|
) => inferSearchInteractionDefinition<TSearchInteraction>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const globalCommandGroup = createGroup<Command>({
|
||||||
|
keyPath: "commandKey",
|
||||||
|
title: "Global commands",
|
||||||
|
useInteraction: (option, query) => option.useInteraction(option, query),
|
||||||
|
Component: ({ icon: Icon, name }) => (
|
||||||
|
<Group px="md" py="sm">
|
||||||
|
<Icon stroke={1.5} />
|
||||||
|
<Text>{name}</Text>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
filter(query, option) {
|
||||||
|
return option.name.toLowerCase().includes(query.toLowerCase());
|
||||||
|
},
|
||||||
|
useOptions() {
|
||||||
|
const tOption = useScopedI18n("search.mode.command.group.globalCommand.option");
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
const commands: (Command & { hidden?: boolean })[] = [
|
||||||
|
{
|
||||||
|
commandKey: "colorScheme",
|
||||||
|
icon: colorScheme === "dark" ? IconSun : IconMoon,
|
||||||
|
name: tOption(`colorScheme.${colorScheme === "dark" ? "light" : "dark"}`),
|
||||||
|
useInteraction: () => {
|
||||||
|
const { toggleColorScheme } = useMantineColorScheme();
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "javaScript",
|
||||||
|
onSelect: toggleColorScheme,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "language",
|
||||||
|
icon: IconLanguage,
|
||||||
|
name: tOption("language.label"),
|
||||||
|
useInteraction: interaction.children(languageChildrenOptions),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "newBoard",
|
||||||
|
icon: IconCategoryPlus,
|
||||||
|
name: tOption("newBoard.label"),
|
||||||
|
useInteraction() {
|
||||||
|
const { openModal } = useModalAction(AddBoardModal);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "javaScript",
|
||||||
|
onSelect() {
|
||||||
|
openModal(undefined);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hidden: !session?.user.permissions.includes("board-create"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "importBoard",
|
||||||
|
icon: IconFileImport,
|
||||||
|
name: tOption("importBoard.label"),
|
||||||
|
useInteraction() {
|
||||||
|
const { openModal } = useModalAction(ImportBoardModal);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "javaScript",
|
||||||
|
onSelect() {
|
||||||
|
openModal(undefined);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hidden: !session?.user.permissions.includes("board-create"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "newApp",
|
||||||
|
icon: IconBox,
|
||||||
|
name: tOption("newApp.label"),
|
||||||
|
useInteraction: interaction.link(() => ({ href: "/manage/apps/new" })),
|
||||||
|
hidden: !session?.user.permissions.includes("app-create"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "newIntegration",
|
||||||
|
icon: IconPlug,
|
||||||
|
name: tOption("newIntegration.label"),
|
||||||
|
useInteraction: interaction.children(newIntegrationChildrenOptions),
|
||||||
|
hidden: !session?.user.permissions.includes("integration-create"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "newUser",
|
||||||
|
icon: IconUserPlus,
|
||||||
|
name: tOption("newUser.label"),
|
||||||
|
useInteraction: interaction.link(() => ({ href: "/manage/users/new" })),
|
||||||
|
hidden: !session?.user.permissions.includes("admin"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "newInvite",
|
||||||
|
icon: IconMailForward,
|
||||||
|
name: tOption("newInvite.label"),
|
||||||
|
useInteraction() {
|
||||||
|
const { openModal } = useModalAction(InviteCreateModal);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "javaScript",
|
||||||
|
onSelect() {
|
||||||
|
openModal(undefined);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hidden: !session?.user.permissions.includes("admin"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "newGroup",
|
||||||
|
icon: IconUsersGroup,
|
||||||
|
name: tOption("newGroup.label"),
|
||||||
|
useInteraction() {
|
||||||
|
const { openModal } = useModalAction(AddGroupModal);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "javaScript",
|
||||||
|
onSelect() {
|
||||||
|
openModal(undefined);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hidden: !session?.user.permissions.includes("admin"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return commands.filter((command) => !command.hidden);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,173 +1,9 @@
|
|||||||
import { Group, Text, useMantineColorScheme } from "@mantine/core";
|
|
||||||
import {
|
|
||||||
IconBox,
|
|
||||||
IconCategoryPlus,
|
|
||||||
IconFileImport,
|
|
||||||
IconLanguage,
|
|
||||||
IconMailForward,
|
|
||||||
IconMoon,
|
|
||||||
IconPlug,
|
|
||||||
IconSun,
|
|
||||||
IconUserPlus,
|
|
||||||
IconUsersGroup,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
|
|
||||||
import { useSession } from "@homarr/auth/client";
|
|
||||||
import { useModalAction } from "@homarr/modals";
|
|
||||||
import { AddBoardModal, AddGroupModal, ImportBoardModal, InviteCreateModal } from "@homarr/modals-collection";
|
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
|
||||||
import type { TablerIcon } from "@homarr/ui";
|
|
||||||
|
|
||||||
import { createGroup } from "../../lib/group";
|
|
||||||
import type { inferSearchInteractionDefinition, SearchInteraction } from "../../lib/interaction";
|
|
||||||
import { interaction } from "../../lib/interaction";
|
|
||||||
import type { SearchMode } from "../../lib/mode";
|
import type { SearchMode } from "../../lib/mode";
|
||||||
import { languageChildrenOptions } from "./children/language";
|
import { contextSpecificActionsSearchGroups } from "./context-specific-group";
|
||||||
import { newIntegrationChildrenOptions } from "./children/new-integration";
|
import { globalCommandGroup } from "./global-group";
|
||||||
|
|
||||||
// This has to be type so it can be interpreted as Record<string, unknown>.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
||||||
type Command<TSearchInteraction extends SearchInteraction = SearchInteraction> = {
|
|
||||||
commandKey: string;
|
|
||||||
icon: TablerIcon;
|
|
||||||
name: string;
|
|
||||||
useInteraction: (
|
|
||||||
_c: Command<TSearchInteraction>,
|
|
||||||
query: string,
|
|
||||||
) => inferSearchInteractionDefinition<TSearchInteraction>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const commandMode = {
|
export const commandMode = {
|
||||||
modeKey: "command",
|
modeKey: "command",
|
||||||
character: ">",
|
character: ">",
|
||||||
groups: [
|
groups: [contextSpecificActionsSearchGroups, globalCommandGroup],
|
||||||
createGroup<Command>({
|
|
||||||
keyPath: "commandKey",
|
|
||||||
title: "Global commands",
|
|
||||||
useInteraction: (option, query) => option.useInteraction(option, query),
|
|
||||||
Component: ({ icon: Icon, name }) => (
|
|
||||||
<Group px="md" py="sm">
|
|
||||||
<Icon stroke={1.5} />
|
|
||||||
<Text>{name}</Text>
|
|
||||||
</Group>
|
|
||||||
),
|
|
||||||
filter(query, option) {
|
|
||||||
return option.name.toLowerCase().includes(query.toLowerCase());
|
|
||||||
},
|
|
||||||
useOptions() {
|
|
||||||
const tOption = useScopedI18n("search.mode.command.group.globalCommand.option");
|
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const { data: session } = useSession();
|
|
||||||
|
|
||||||
const commands: (Command & { hidden?: boolean })[] = [
|
|
||||||
{
|
|
||||||
commandKey: "colorScheme",
|
|
||||||
icon: colorScheme === "dark" ? IconSun : IconMoon,
|
|
||||||
name: tOption(`colorScheme.${colorScheme === "dark" ? "light" : "dark"}`),
|
|
||||||
useInteraction: () => {
|
|
||||||
const { toggleColorScheme } = useMantineColorScheme();
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "javaScript",
|
|
||||||
onSelect: toggleColorScheme,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "language",
|
|
||||||
icon: IconLanguage,
|
|
||||||
name: tOption("language.label"),
|
|
||||||
useInteraction: interaction.children(languageChildrenOptions),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "newBoard",
|
|
||||||
icon: IconCategoryPlus,
|
|
||||||
name: tOption("newBoard.label"),
|
|
||||||
useInteraction() {
|
|
||||||
const { openModal } = useModalAction(AddBoardModal);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "javaScript",
|
|
||||||
onSelect() {
|
|
||||||
openModal(undefined);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
hidden: !session?.user.permissions.includes("board-create"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "importBoard",
|
|
||||||
icon: IconFileImport,
|
|
||||||
name: tOption("importBoard.label"),
|
|
||||||
useInteraction() {
|
|
||||||
const { openModal } = useModalAction(ImportBoardModal);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "javaScript",
|
|
||||||
onSelect() {
|
|
||||||
openModal(undefined);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
hidden: !session?.user.permissions.includes("board-create"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "newApp",
|
|
||||||
icon: IconBox,
|
|
||||||
name: tOption("newApp.label"),
|
|
||||||
useInteraction: interaction.link(() => ({ href: "/manage/apps/new" })),
|
|
||||||
hidden: !session?.user.permissions.includes("app-create"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "newIntegration",
|
|
||||||
icon: IconPlug,
|
|
||||||
name: tOption("newIntegration.label"),
|
|
||||||
useInteraction: interaction.children(newIntegrationChildrenOptions),
|
|
||||||
hidden: !session?.user.permissions.includes("integration-create"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "newUser",
|
|
||||||
icon: IconUserPlus,
|
|
||||||
name: tOption("newUser.label"),
|
|
||||||
useInteraction: interaction.link(() => ({ href: "/manage/users/new" })),
|
|
||||||
hidden: !session?.user.permissions.includes("admin"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "newInvite",
|
|
||||||
icon: IconMailForward,
|
|
||||||
name: tOption("newInvite.label"),
|
|
||||||
useInteraction() {
|
|
||||||
const { openModal } = useModalAction(InviteCreateModal);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "javaScript",
|
|
||||||
onSelect() {
|
|
||||||
openModal(undefined);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
hidden: !session?.user.permissions.includes("admin"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "newGroup",
|
|
||||||
icon: IconUsersGroup,
|
|
||||||
name: tOption("newGroup.label"),
|
|
||||||
useInteraction() {
|
|
||||||
const { openModal } = useModalAction(AddGroupModal);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "javaScript",
|
|
||||||
onSelect() {
|
|
||||||
openModal(undefined);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
hidden: !session?.user.permissions.includes("admin"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return commands.filter((command) => !command.hidden);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
} satisfies SearchMode;
|
} satisfies SearchMode;
|
||||||
|
|||||||
34
packages/spotlight/src/modes/home/context-specific-group.tsx
Normal file
34
packages/spotlight/src/modes/home/context-specific-group.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Group, Text } from "@mantine/core";
|
||||||
|
|
||||||
|
import { createGroup } from "../../lib/group";
|
||||||
|
import type { ContextSpecificItem } from "./context";
|
||||||
|
import { useSpotlightContextResults } from "./context";
|
||||||
|
|
||||||
|
export const contextSpecificSearchGroups = createGroup<ContextSpecificItem>({
|
||||||
|
title: (t) => t("search.mode.home.group.local.title"),
|
||||||
|
keyPath: "id",
|
||||||
|
Component(option) {
|
||||||
|
const icon =
|
||||||
|
typeof option.icon !== "string" ? (
|
||||||
|
<option.icon size={24} />
|
||||||
|
) : (
|
||||||
|
<img width={24} height={24} src={option.icon} alt={option.name} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group w="100%" wrap="nowrap" align="center" px="md" py="xs">
|
||||||
|
{icon}
|
||||||
|
<Text>{option.name}</Text>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
useInteraction(option) {
|
||||||
|
return option.interaction();
|
||||||
|
},
|
||||||
|
filter(query, option) {
|
||||||
|
return option.name.toLowerCase().includes(query.toLowerCase());
|
||||||
|
},
|
||||||
|
useOptions() {
|
||||||
|
return useSpotlightContextResults();
|
||||||
|
},
|
||||||
|
});
|
||||||
122
packages/spotlight/src/modes/home/context.tsx
Normal file
122
packages/spotlight/src/modes/home/context.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import type { DependencyList, PropsWithChildren } from "react";
|
||||||
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import type { TablerIcon } from "@homarr/ui";
|
||||||
|
|
||||||
|
import type { inferSearchInteractionDefinition, SearchInteraction } from "../../lib/interaction";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||||
|
export type ContextSpecificItem = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon: TablerIcon | string;
|
||||||
|
interaction: () => inferSearchInteractionDefinition<SearchInteraction>;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SpotlightContextProps {
|
||||||
|
items: ContextSpecificItem[];
|
||||||
|
registerItems: (key: string, results: ContextSpecificItem[]) => void;
|
||||||
|
unregisterItems: (key: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSpotlightContext = (displayName: string) => {
|
||||||
|
const SpotlightContext = createContext<SpotlightContextProps | null>(null);
|
||||||
|
SpotlightContext.displayName = displayName;
|
||||||
|
|
||||||
|
const Provider = ({ children }: PropsWithChildren) => {
|
||||||
|
const [itemsMap, setItemsMap] = useState<Map<string, { items: ContextSpecificItem[]; count: number }>>(new Map());
|
||||||
|
|
||||||
|
const registerItems = useCallback((key: string, newItems: ContextSpecificItem[]) => {
|
||||||
|
setItemsMap((prevItems) => {
|
||||||
|
const newItemsMap = new Map(prevItems);
|
||||||
|
newItemsMap.set(key, { items: newItems, count: (newItemsMap.get(key)?.count ?? 0) + 1 });
|
||||||
|
return newItemsMap;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const unregisterItems = useCallback((key: string) => {
|
||||||
|
setItemsMap((prevItems) => {
|
||||||
|
const registrationCount = prevItems.get(key)?.count ?? 0;
|
||||||
|
|
||||||
|
if (registrationCount <= 1) {
|
||||||
|
const newItemsMap = new Map(prevItems);
|
||||||
|
newItemsMap.delete(key);
|
||||||
|
return newItemsMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newItemsMap = new Map(prevItems);
|
||||||
|
newItemsMap.set(key, { items: newItemsMap.get(key)?.items ?? [], count: registrationCount - 1 });
|
||||||
|
|
||||||
|
return prevItems;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const items = useMemo(() => Array.from(itemsMap.values()).flatMap(({ items }) => items), [itemsMap]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SpotlightContext.Provider value={{ items, registerItems, unregisterItems }}>
|
||||||
|
{children}
|
||||||
|
</SpotlightContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSpotlightContextItems = () => {
|
||||||
|
const context = useContext(SpotlightContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(`useSpotlightContextItems must be used within SpotlightContext[displayName=${displayName}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.items;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useRegisterSpotlightContextItems = (
|
||||||
|
key: string,
|
||||||
|
items: ContextSpecificItem[],
|
||||||
|
dependencyArray: DependencyList,
|
||||||
|
) => {
|
||||||
|
const context = useContext(SpotlightContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
`useRegisterSpotlightContextItems must be used within SpotlightContext[displayName=${displayName}]`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
context.registerItems(
|
||||||
|
key,
|
||||||
|
items.filter((item) => !item.disabled),
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
context.unregisterItems(key);
|
||||||
|
};
|
||||||
|
// We ignore the results
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [...dependencyArray, key]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [SpotlightContext, Provider, useSpotlightContextItems, useRegisterSpotlightContextItems] as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [_ResultContext, ResultProvider, useSpotlightContextResults, useRegisterSpotlightContextResults] =
|
||||||
|
createSpotlightContext("SpotlightContextSpecificResults");
|
||||||
|
const [_ActionContext, ActionProvider, useSpotlightContextActions, useRegisterSpotlightContextActions] =
|
||||||
|
createSpotlightContext("SpotlightContextSpecificActions");
|
||||||
|
|
||||||
|
export {
|
||||||
|
useRegisterSpotlightContextActions,
|
||||||
|
useRegisterSpotlightContextResults,
|
||||||
|
useSpotlightContextActions,
|
||||||
|
useSpotlightContextResults,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpotlightProvider = ({ children }: PropsWithChildren) => {
|
||||||
|
return (
|
||||||
|
<ResultProvider>
|
||||||
|
<ActionProvider>{children}</ActionProvider>
|
||||||
|
</ResultProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
8
packages/spotlight/src/modes/home/index.tsx
Normal file
8
packages/spotlight/src/modes/home/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import type { SearchMode } from "../../lib/mode";
|
||||||
|
import { contextSpecificSearchGroups } from "./context-specific-group";
|
||||||
|
|
||||||
|
export const homeMode = {
|
||||||
|
character: undefined,
|
||||||
|
modeKey: "home",
|
||||||
|
groups: [contextSpecificSearchGroups],
|
||||||
|
} satisfies SearchMode;
|
||||||
@@ -11,10 +11,11 @@ import type { SearchMode } from "../lib/mode";
|
|||||||
import { appIntegrationBoardMode } from "./app-integration-board";
|
import { appIntegrationBoardMode } from "./app-integration-board";
|
||||||
import { commandMode } from "./command";
|
import { commandMode } from "./command";
|
||||||
import { externalMode } from "./external";
|
import { externalMode } from "./external";
|
||||||
|
import { homeMode } from "./home";
|
||||||
import { pageMode } from "./page";
|
import { pageMode } from "./page";
|
||||||
import { userGroupMode } from "./user-group";
|
import { userGroupMode } from "./user-group";
|
||||||
|
|
||||||
const searchModesWithoutHelp = [userGroupMode, appIntegrationBoardMode, externalMode, commandMode, pageMode] as const;
|
const searchModesForHelp = [userGroupMode, appIntegrationBoardMode, externalMode, commandMode, pageMode] as const;
|
||||||
|
|
||||||
const helpMode = {
|
const helpMode = {
|
||||||
modeKey: "help",
|
modeKey: "help",
|
||||||
@@ -82,4 +83,4 @@ const helpMode = {
|
|||||||
},
|
},
|
||||||
} satisfies SearchMode;
|
} satisfies SearchMode;
|
||||||
|
|
||||||
export const searchModes = [...searchModesWithoutHelp, helpMode] as const;
|
export const searchModes = [...searchModesForHelp, helpMode, homeMode] as const;
|
||||||
|
|||||||
@@ -32,15 +32,15 @@
|
|||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"deepmerge": "4.3.1",
|
"deepmerge": "4.3.1",
|
||||||
"mantine-react-table": "2.0.0-beta.7",
|
"mantine-react-table": "2.0.0-beta.7",
|
||||||
"next": "^14.2.18",
|
"next": "^14.2.20",
|
||||||
"next-intl": "3.25.3",
|
"next-intl": "3.26.0",
|
||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@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",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1159,6 +1159,9 @@
|
|||||||
"automationId": {
|
"automationId": {
|
||||||
"label": "Automation ID"
|
"label": "Automation ID"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"spotlightAction": {
|
||||||
|
"run": "Run {name}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
@@ -2450,6 +2453,9 @@
|
|||||||
"command": {
|
"command": {
|
||||||
"help": "Activate command mode",
|
"help": "Activate command mode",
|
||||||
"group": {
|
"group": {
|
||||||
|
"localCommand": {
|
||||||
|
"title": "Local commands"
|
||||||
|
},
|
||||||
"globalCommand": {
|
"globalCommand": {
|
||||||
"title": "Global commands",
|
"title": "Global commands",
|
||||||
"option": {
|
"option": {
|
||||||
@@ -2559,6 +2565,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"home": {
|
||||||
|
"group": {
|
||||||
|
"local": {
|
||||||
|
"title": "Local results"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"help": "Search for pages",
|
"help": "Search for pages",
|
||||||
"group": {
|
"group": {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2734
packages/translation/src/lang/zh.json
Normal file
2734
packages/translation/src/lang/zh.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
import deepmerge from "deepmerge";
|
import deepmerge from "deepmerge";
|
||||||
import { getRequestConfig } from "next-intl/server";
|
import { getRequestConfig } from "next-intl/server";
|
||||||
|
|
||||||
|
import type { TranslationObject } from ".";
|
||||||
import { fallbackLocale, isLocaleSupported } from ".";
|
import { fallbackLocale, isLocaleSupported } from ".";
|
||||||
import type { SupportedLanguage } from "./config";
|
import type { SupportedLanguage } from "./config";
|
||||||
import { createLanguageMapping } from "./mapping";
|
import { createLanguageMapping } from "./mapping";
|
||||||
@@ -15,7 +16,7 @@ export default getRequestConfig(async ({ requestLocale }) => {
|
|||||||
const typedLocale = currentLocale as SupportedLanguage;
|
const typedLocale = currentLocale as SupportedLanguage;
|
||||||
|
|
||||||
const languageMap = createLanguageMapping();
|
const languageMap = createLanguageMapping();
|
||||||
const currentMessages = (await languageMap[typedLocale]()).default;
|
const currentMessages = removeEmptyTranslations((await languageMap[typedLocale]()).default) as TranslationObject;
|
||||||
|
|
||||||
// Fallback to default locale if the current locales messages if not all messages are present
|
// Fallback to default locale if the current locales messages if not all messages are present
|
||||||
if (currentLocale !== fallbackLocale) {
|
if (currentLocale !== fallbackLocale) {
|
||||||
@@ -31,3 +32,26 @@ export default getRequestConfig(async ({ requestLocale }) => {
|
|||||||
messages: currentMessages,
|
messages: currentMessages,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const removeEmptyTranslations = (translations: Record<string, unknown>): Record<string, unknown> => {
|
||||||
|
return Object.entries(translations).reduce(
|
||||||
|
(acc, [key, value]) => {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[key]: removeEmptyTranslations(value as Record<string, unknown>),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.trim() === "") {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{} as Record<string, unknown>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user