diff --git a/data/constants.ts b/data/constants.ts index 5556a8966..b1ff95a49 100644 --- a/data/constants.ts +++ b/data/constants.ts @@ -1,3 +1,3 @@ export const REPO_URL = 'ajnart/homarr'; -export const CURRENT_VERSION = 'v0.11.2'; +export const CURRENT_VERSION = 'v0.11.3'; export const ICON_PICKER_SLICE_LIMIT = 36; diff --git a/package.json b/package.json index 41ffd4c40..ea144ed3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homarr", - "version": "0.11.2", + "version": "0.11.3", "description": "Homarr - A homepage for your server.", "license": "MIT", "repository": { diff --git a/public/locales/da/layout/modals/icon-picker.json b/public/locales/da/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/da/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/de/layout/modals/icon-picker.json b/public/locales/de/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/de/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/el/layout/modals/icon-picker.json b/public/locales/el/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/el/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/el/settings/common.json b/public/locales/el/settings/common.json index 3576da775..d5ae45d91 100644 --- a/public/locales/el/settings/common.json +++ b/public/locales/el/settings/common.json @@ -2,7 +2,7 @@ "title": "Ρυθμίσεις", "tooltip": "Ρυθμίσεις", "tabs": { - "common": "Συχνά", + "common": "Συχνές επιλογές", "customizations": "Παραμετροποιήσεις" }, "tips": { diff --git a/public/locales/el/settings/general/config-changer.json b/public/locales/el/settings/general/config-changer.json index 86b6a9a06..4fe61e49c 100644 --- a/public/locales/el/settings/general/config-changer.json +++ b/public/locales/el/settings/general/config-changer.json @@ -62,7 +62,7 @@ } } }, - "saveCopy": "Αποθήκευση αντιγράφου" + "saveCopy": "Αποθηκεύστε ένα αντίγραφο" }, "dropzone": { "notifications": { diff --git a/public/locales/en/layout/modals/icon-picker.json b/public/locales/en/layout/modals/icon-picker.json new file mode 100644 index 000000000..84f17ce54 --- /dev/null +++ b/public/locales/en/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "Search something...", + "searchLimitationTitle": "Limited to 30 results", + "searchLimitationMessage": "Search results were limited to 30 because there were too many matches" + } +} \ No newline at end of file diff --git a/public/locales/es/layout/modals/icon-picker.json b/public/locales/es/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/es/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/fr/layout/modals/icon-picker.json b/public/locales/fr/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/fr/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/he/layout/modals/icon-picker.json b/public/locales/he/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/he/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/it/layout/modals/icon-picker.json b/public/locales/it/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/it/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/it/modules/torrents-status.json b/public/locales/it/modules/torrents-status.json index 4b20d52fe..7b5ea8e30 100644 --- a/public/locales/it/modules/torrents-status.json +++ b/public/locales/it/modules/torrents-status.json @@ -18,7 +18,7 @@ "card": { "footer": { "error": "Errore", - "lastUpdated": "Ultimo aggiornamento {{time}} ago" + "lastUpdated": "Ultimo aggiornamento {{time}} fa" }, "table": { "header": { @@ -30,7 +30,7 @@ "progress": "Avanzamento" }, "item": { - "text": "Gestito da {{appName}}, rapporto {{ratio}}" + "text": "Gestito da {{appName}}, {{ratio}} ratio" }, "body": { "nothingFound": "Nessun torrent trovato" @@ -61,10 +61,10 @@ "introductionPrefix": "Gestito da", "metrics": { "queuePosition": "Posizione in coda - {{position}}", - "progress": "Progressi - {{progress}}%", + "progress": "Progresso - {{progress}}%", "totalSelectedSize": "Totale - {{totalSize}}", "state": "Stato - {{state}}", - "ratio": "Rapporto -", + "ratio": "Ratio -", "completed": "Completato" } } diff --git a/public/locales/ja/layout/modals/icon-picker.json b/public/locales/ja/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/ja/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/ko/layout/modals/icon-picker.json b/public/locales/ko/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/ko/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/lol/layout/modals/icon-picker.json b/public/locales/lol/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/lol/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/nl/layout/modals/icon-picker.json b/public/locales/nl/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/nl/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/pl/layout/modals/icon-picker.json b/public/locales/pl/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/pl/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/pt/layout/modals/icon-picker.json b/public/locales/pt/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/pt/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/ru/layout/modals/icon-picker.json b/public/locales/ru/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/ru/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/sl/layout/modals/icon-picker.json b/public/locales/sl/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/sl/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/sv/layout/modals/about.json b/public/locales/sv/layout/modals/about.json index e35dbbfed..ffd97b0b8 100644 --- a/public/locales/sv/layout/modals/about.json +++ b/public/locales/sv/layout/modals/about.json @@ -3,5 +3,5 @@ "i18n": "Laddade namnområden för I18n-översättningar", "locales": "Konfigurerade I18n lokalspråk", "contact": "Har du problem eller frågor? Kontakta oss!", - "addToDashboard": "Lägg till i instrumentpanel" + "addToDashboard": "Lägg till på instrumentpanel" } diff --git a/public/locales/sv/layout/modals/add-app.json b/public/locales/sv/layout/modals/add-app.json index 923c67f0d..7f7c8b6e8 100644 --- a/public/locales/sv/layout/modals/add-app.json +++ b/public/locales/sv/layout/modals/add-app.json @@ -39,7 +39,7 @@ "appearance": { "icon": { "label": "Appikon", - "description": "Ikonen som kommer att visas på instrumentpanelen." + "description": "Ikon som kommer att visas på instrumentpanelen." } }, "integration": { diff --git a/public/locales/sv/layout/modals/change-position.json b/public/locales/sv/layout/modals/change-position.json index 5d1714b75..2a358c34d 100644 --- a/public/locales/sv/layout/modals/change-position.json +++ b/public/locales/sv/layout/modals/change-position.json @@ -1,8 +1,8 @@ { - "xPosition": "X axel position", + "xPosition": "Position X-axel", "width": "Bredd", "height": "Höjd", - "yPosition": "Y axel position", + "yPosition": "Position Y-axel", "zeroOrHigher": "0 eller högre", "betweenXandY": "Mellan {{min}} och {{max}}" } \ No newline at end of file diff --git a/public/locales/sv/layout/modals/icon-picker.json b/public/locales/sv/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/sv/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/sv/modules/dashdot.json b/public/locales/sv/modules/dashdot.json index 2ae7fa75a..d6a019e56 100644 --- a/public/locales/sv/modules/dashdot.json +++ b/public/locales/sv/modules/dashdot.json @@ -8,7 +8,7 @@ "label": "Flerkärnig CPU vy" }, "storageMultiView": { - "label": "Visning av flera lagrings enheter" + "label": "Visning av flera lagringsenheter" }, "useCompactView": { "label": "Använd kompakt vy" diff --git a/public/locales/uk/layout/modals/icon-picker.json b/public/locales/uk/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/uk/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/uk/modules/torrents-status.json b/public/locales/uk/modules/torrents-status.json index f6df37230..69e8e03bc 100644 --- a/public/locales/uk/modules/torrents-status.json +++ b/public/locales/uk/modules/torrents-status.json @@ -58,12 +58,12 @@ "title": "Завантаження..." }, "popover": { - "introductionPrefix": "Під керівництвом", + "introductionPrefix": "Керується", "metrics": { "queuePosition": "Позиція в черзі - {{position}}", "progress": "Прогрес - {{progress}}%.", "totalSelectedSize": "Всього - {{totalSize}}", - "state": "Держава - {{state}}", + "state": "Стан - {{state}}", "ratio": "Коефіцієнт -", "completed": "Завершено" } diff --git a/public/locales/vi/layout/modals/icon-picker.json b/public/locales/vi/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/vi/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/zh/layout/modals/icon-picker.json b/public/locales/zh/layout/modals/icon-picker.json new file mode 100644 index 000000000..349810cb9 --- /dev/null +++ b/public/locales/zh/layout/modals/icon-picker.json @@ -0,0 +1,7 @@ +{ + "iconPicker": { + "textInputPlaceholder": "", + "searchLimitationTitle": "", + "searchLimitationMessage": "" + } +} \ No newline at end of file diff --git a/public/locales/zh/settings/common.json b/public/locales/zh/settings/common.json index f8d028289..fffc224de 100644 --- a/public/locales/zh/settings/common.json +++ b/public/locales/zh/settings/common.json @@ -22,7 +22,7 @@ "enablelsidebar": "启用左边的侧边栏", "enablesearchbar": "启用搜索栏", "enabledocker": "启用docker集成", - "enableping": "启用平移功能", + "enableping": "启用Ping功能", "enablelsidebardesc": "可选的。只能用于应用程序和集成", "enablersidebardesc": "可选的。只能用于应用程序和集成" } diff --git a/src/components/Dashboard/Modals/EditAppModal/Tabs/AppereanceTab/AppereanceTab.tsx b/src/components/Dashboard/Modals/EditAppModal/Tabs/AppereanceTab/AppereanceTab.tsx index 8e46d0f51..f74ab7d60 100644 --- a/src/components/Dashboard/Modals/EditAppModal/Tabs/AppereanceTab/AppereanceTab.tsx +++ b/src/components/Dashboard/Modals/EditAppModal/Tabs/AppereanceTab/AppereanceTab.tsx @@ -1,5 +1,6 @@ -import { createStyles, Flex, Tabs, TextInput } from '@mantine/core'; +import { Autocomplete, createStyles, Flex, Tabs, TextInput } from '@mantine/core'; import { UseFormReturnType } from '@mantine/form'; +import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'next-i18next'; import { AppType } from '../../../../../../types/app'; import { DebouncedAppIcon } from '../Shared/DebouncedAppIcon'; @@ -18,16 +19,21 @@ export const AppearanceTab = ({ }: AppearanceTabProps) => { const { t } = useTranslation('layout/modals/add-app'); const { classes } = useStyles(); + const { isLoading, error, data } = useQuery({ + queryKey: ['autocompleteLocale'], + queryFn: () => fetch('/api/getLocalImages').then((res) => res.json()), + }); return ( - } label={t('appearance.icon.label')} description={t('appearance.icon.description')} variant="default" + data={data?.files ?? []} withAsterisk required {...form.getInputProps('appearance.iconUrl')} diff --git a/src/components/Dashboard/Modals/EditAppModal/Tabs/AppereanceTab/IconSelector/IconSelector.tsx b/src/components/Dashboard/Modals/EditAppModal/Tabs/AppereanceTab/IconSelector/IconSelector.tsx index 8ec13cbb3..ee853678a 100644 --- a/src/components/Dashboard/Modals/EditAppModal/Tabs/AppereanceTab/IconSelector/IconSelector.tsx +++ b/src/components/Dashboard/Modals/EditAppModal/Tabs/AppereanceTab/IconSelector/IconSelector.tsx @@ -31,7 +31,7 @@ interface IconSelectorProps { } export const IconSelector = ({ onChange, allowAppNamePropagation, form }: IconSelectorProps) => { - const { t } = useTranslation('layout/tools'); + const { t } = useTranslation('layout/modals/icon-picker'); const { data, isLoading } = useRepositoryIconsQuery({ url: 'https://api.github.com/repos/walkxcode/Dashboard-Icons/contents/png', diff --git a/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx b/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx index edb689821..d746ae334 100644 --- a/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx +++ b/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx @@ -50,7 +50,7 @@ export const AvailableElementTypes = ({ { id: uuidv4(), // Thank you ChatGPT ;) - position: previousConfig.wrappers.length + 1, + position: previousConfig.categories.length + 1, }, ], categories: [ diff --git a/src/components/Dashboard/Wrappers/Category/Category.tsx b/src/components/Dashboard/Wrappers/Category/Category.tsx index 9034a2e1a..f2fda5028 100644 --- a/src/components/Dashboard/Wrappers/Category/Category.tsx +++ b/src/components/Dashboard/Wrappers/Category/Category.tsx @@ -1,6 +1,8 @@ -import { Group, Title } from '@mantine/core'; +import { Accordion, Title } from '@mantine/core'; +import { useLocalStorage } from '@mantine/hooks'; +import { useConfigContext } from '../../../../config/provider'; import { CategoryType } from '../../../../types/category'; -import { HomarrCardWrapper } from '../../Tiles/HomarrCardWrapper'; +import { useCardStyles } from '../../../layout/useCardStyles'; import { useEditModeStore } from '../../Views/useEditModeStore'; import { useGridstack } from '../gridstack/use-gridstack'; import { WrapperContent } from '../WrapperContent'; @@ -13,20 +15,47 @@ interface DashboardCategoryProps { export const DashboardCategory = ({ category }: DashboardCategoryProps) => { const { refs, apps, widgets } = useGridstack('category', category.id); const isEditMode = useEditModeStore((x) => x.enabled); + const { config } = useConfigContext(); + const { classes: cardClasses } = useCardStyles(true); + + const categoryList = config?.categories.map((x) => x.name) ?? []; + const [toggledCategories, setToggledCategories] = useLocalStorage({ + key: `${config?.configProperties.name}-app-shelf-toggled`, + // This is a bit of a hack to toggle the categories on the first load, return a string[] of the categories + defaultValue: categoryList, + }); return ( - - - {category.name} - {isEditMode ? : null} - -
- -
-
+ { + // Cancel if edit mode is on + if (isEditMode) return; + setToggledCategories([...state]); + }} + > + + }> + {category.name} + + +
+ +
+
+
+
); }; diff --git a/src/components/Dashboard/Wrappers/Category/CategoryEditMenu.tsx b/src/components/Dashboard/Wrappers/Category/CategoryEditMenu.tsx index 24c89bc8f..70b1ed85c 100644 --- a/src/components/Dashboard/Wrappers/Category/CategoryEditMenu.tsx +++ b/src/components/Dashboard/Wrappers/Category/CategoryEditMenu.tsx @@ -22,7 +22,7 @@ export const CategoryEditMenu = ({ category }: CategoryEditMenuProps) => { useCategoryActions(configName, category); return ( - + diff --git a/src/components/Dashboard/Wrappers/Category/useCategoryActions.tsx b/src/components/Dashboard/Wrappers/Category/useCategoryActions.tsx index 777bdabf3..2ea0698ee 100644 --- a/src/components/Dashboard/Wrappers/Category/useCategoryActions.tsx +++ b/src/components/Dashboard/Wrappers/Category/useCategoryActions.tsx @@ -1,8 +1,11 @@ import { v4 as uuidv4 } from 'uuid'; import { useConfigStore } from '../../../../config/store'; import { openContextModalGeneric } from '../../../../tools/mantineModalManagerExtensions'; +import { AppType } from '../../../../types/app'; import { CategoryType } from '../../../../types/category'; +import { ConfigType } from '../../../../types/config'; import { WrapperType } from '../../../../types/wrapper'; +import { IWidget } from '../../../../widgets/widgets'; import { CategoryEditModalInnerProps } from './CategoryEditModal'; export const useCategoryActions = (configName: string | undefined, category: CategoryType) => { @@ -185,29 +188,67 @@ export const useCategoryActions = (configName: string | undefined, category: Cat const currentItem = previous.categories.find((x) => x.id === category.id); if (!currentItem) return previous; // Find the main wrapper - const mainWrapper = previous.wrappers.find((x) => x.position === 1); + const mainWrapper = previous.wrappers.find((x) => x.position === 0); + const mainWrapperId = mainWrapper?.id ?? 'default'; - // Check that the app has an area.type or "category" and that the area.id is the current category - const appsToMove = previous.apps.filter( - (x) => x.area && x.area.type === 'category' && x.area.properties.id === currentItem.id - ); - appsToMove.forEach((x) => { - // eslint-disable-next-line no-param-reassign - x.area = { type: 'wrapper', properties: { id: mainWrapper?.id ?? 'default' } }; - }); + const isAppAffectedFilter = (app: AppType): boolean => { + if (!app.area) { + return false; + } - const widgetsToMove = previous.widgets.filter( - (x) => x.area && x.area.type === 'category' && x.area.properties.id === currentItem.id - ); + if (app.area.type !== 'category') { + return false; + } - widgetsToMove.forEach((x) => { - // eslint-disable-next-line no-param-reassign - x.area = { type: 'wrapper', properties: { id: mainWrapper?.id ?? 'default' } }; - }); + return app.area.properties.id !== mainWrapperId; + }; + + const isWidgetAffectedFilter = (widget: IWidget): boolean => { + if (!widget.area) { + return false; + } + + if (widget.area.type !== 'category') { + return false; + } + + return widget.area.properties.id !== mainWrapperId; + }; return { ...previous, - apps: previous.apps, + apps: [ + ...previous.apps.filter((x) => !isAppAffectedFilter(x)), + ...previous.apps + .filter((x) => isAppAffectedFilter(x)) + .map((app): AppType => ({ + ...app, + area: { + ...app.area, + type: 'wrapper', + properties: { + ...app.area.properties, + id: mainWrapperId, + }, + }, + })), + ], + widgets: [ + ...previous.widgets.filter((widget) => !isWidgetAffectedFilter(widget)), + ...previous.widgets + .filter((widget) => isWidgetAffectedFilter(widget)) + .map((widget): IWidget => ({ + ...widget, + area: { + ...widget.area, + type: 'wrapper', + properties: { + ...widget.area.properties, + id: mainWrapperId, + }, + }, + })), + ], categories: previous.categories.filter((x) => x.id !== category.id), wrappers: previous.wrappers.filter((x) => x.position !== currentItem.position), }; diff --git a/src/components/layout/header/Search.tsx b/src/components/layout/header/Search.tsx index 0cb7755d2..bf9753425 100644 --- a/src/components/layout/header/Search.tsx +++ b/src/components/layout/header/Search.tsx @@ -13,6 +13,7 @@ import { import { useDebouncedValue, useHotkeys } from '@mantine/hooks'; import { showNotification } from '@mantine/notifications'; import { IconBrandYoutube, IconDownload, IconMovie, IconSearch } from '@tabler/icons'; +import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; import { useTranslation } from 'next-i18next'; import React, { forwardRef, useEffect, useRef, useState } from 'react'; @@ -139,16 +140,27 @@ export function Search() { const openInNewTab = config?.settings.common.searchEngine.properties.openInNewTab ? '_blank' : '_self'; - const [OverseerrResults, setOverseerrResults] = useState([]); const [opened, setOpened] = useState(false); - useEffect(() => { - if (debounced !== '' && selectedSearchEngine.value === 'overseerr' && searchQuery.length > 3) { - axios.get(`/api/modules/overseerr?query=${searchQuery}`).then((res) => { - setOverseerrResults(res.data.results ?? []); - }); + const { + data: OverseerrResults, + isLoading, + error, + } = useQuery( + ['overseerr', debounced], + async () => { + if (debounced !== '' && selectedSearchEngine.value === 'overseerr' && debounced.length > 3) { + const res = await axios.get(`/api/modules/overseerr?query=${debounced}`); + return res.data.results ?? []; + } + return []; + }, + { + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchInterval: false, } - }, [debounced]); + ); const isModuleEnabled = config?.settings.customization.layout.enabledSearchbar; if (!isModuleEnabled) { @@ -159,7 +171,7 @@ export function Search() { return ( 0 && opened && searchQuery.length > 3} + opened={OverseerrResults && OverseerrResults.length > 0 && opened && searchQuery.length > 3} position="bottom" withinPortal shadow="md" @@ -207,16 +219,16 @@ export function Search() { /> -
- - {OverseerrResults.slice(0, 5).map((result, index) => ( - - - {index < OverseerrResults.length - 1 && } - - ))} - -
+ + {OverseerrResults && OverseerrResults.slice(0, 4).map((result: any, index: number) => ( + + + {index < OverseerrResults.length - 1 && index < 3 && ( + + )} + + ))} +
diff --git a/src/hooks/widgets/download-speed/useGetNetworkSpeed.tsx b/src/hooks/widgets/download-speed/useGetNetworkSpeed.tsx new file mode 100644 index 000000000..965607e1c --- /dev/null +++ b/src/hooks/widgets/download-speed/useGetNetworkSpeed.tsx @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import { NormalizedDownloadQueueResponse } from '../../../types/api/downloads/queue/NormalizedDownloadQueueResponse'; + +export const useGetDownloadClientsQueue = () => useQuery({ + queryKey: ['network-speed'], + queryFn: async (): Promise => { + const response = await fetch('/api/modules/downloads'); + return response.json(); + }, + refetchInterval: 3000, +}); diff --git a/src/hooks/widgets/torrents/useGetTorrentData.tsx b/src/hooks/widgets/torrents/useGetTorrentData.tsx deleted file mode 100644 index 8027462e3..000000000 --- a/src/hooks/widgets/torrents/useGetTorrentData.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Query, useQuery } from '@tanstack/react-query'; -import axios from 'axios'; -import { NormalizedTorrentListResponse } from '../../../types/api/NormalizedTorrentListResponse'; - -interface TorrentsDataRequestParams { - appId: string; - refreshInterval: number; -} - -export const useGetTorrentData = (params: TorrentsDataRequestParams) => - useQuery({ - queryKey: ['torrentsData', params.appId], - queryFn: fetchData, - refetchOnWindowFocus: true, - refetchInterval(_: any, query: Query) { - if (query.state.fetchFailureCount < 3) { - return params.refreshInterval; - } - return false; - }, - enabled: !!params.appId, - }); - -const fetchData = async (): Promise => { - const response = await axios.post('/api/modules/torrents'); - return response.data as NormalizedTorrentListResponse; -}; diff --git a/src/modules/Docker/DockerModule.tsx b/src/modules/Docker/DockerModule.tsx index cfffbed00..684178dd9 100644 --- a/src/modules/Docker/DockerModule.tsx +++ b/src/modules/Docker/DockerModule.tsx @@ -1,4 +1,5 @@ import { ActionIcon, Drawer, Text, Tooltip } from '@mantine/core'; +import { useHotkeys } from '@mantine/hooks'; import { showNotification } from '@mantine/notifications'; import { IconBrandDocker, IconX } from '@tabler/icons'; import axios from 'axios'; @@ -17,6 +18,7 @@ export default function DockerMenuButton(props: any) { const [selection, setSelection] = useState([]); const { config } = useConfigContext(); const { classes } = useCardStyles(true); + useHotkeys([['mod+B', () => setOpened(!opened)]]); const dockerEnabled = config?.settings.customization.layout.enabledDocker || false; @@ -60,6 +62,7 @@ export default function DockerMenuButton(props: any) { <> setOpened(false)} padding="xl" position="right" diff --git a/src/modules/Docker/DockerTable.tsx b/src/modules/Docker/DockerTable.tsx index 2925e3990..70f597d23 100644 --- a/src/modules/Docker/DockerTable.tsx +++ b/src/modules/Docker/DockerTable.tsx @@ -7,6 +7,7 @@ import { ScrollArea, TextInput, useMantineTheme, + Text, } from '@mantine/core'; import { useElementSize } from '@mantine/hooks'; import { IconSearch } from '@tabler/icons'; @@ -78,8 +79,16 @@ export default function DockerTable({ transitionDuration={0} /> - {element.Names[0].replace('/', '')} - {width > MIN_WIDTH_MOBILE && {element.Image}} + + + {element.Names[0].replace('/', '')} + + + {width > MIN_WIDTH_MOBILE && ( + + {element.Image} + + )} {width > MIN_WIDTH_MOBILE && ( @@ -111,12 +120,13 @@ export default function DockerTable({ }); return ( - + } value={search} + autoFocus onChange={handleSearchChange} /> diff --git a/src/modules/common/MediaDisplay.tsx b/src/modules/common/MediaDisplay.tsx index 7c3631b18..7fea14508 100644 --- a/src/modules/common/MediaDisplay.tsx +++ b/src/modules/common/MediaDisplay.tsx @@ -180,7 +180,7 @@ export function MediaDisplay({ media }: { media: IMedia }) { const { t } = useTranslation('modules/common-media-cards'); return ( - + @@ -223,7 +223,7 @@ export function MediaDisplay({ media }: { media: IMedia }) { {media.overview} - + {media.plexUrl && ( - {data.torrents.map((concatenatedTorrentList) => { - const app = config?.apps.find((x) => x.id === concatenatedTorrentList.appId); - return concatenatedTorrentList.torrents - .filter(filter) - .map((item: NormalizedTorrent, index: number) => ( - - )); - })} + {torrents.map((torrent, index) => ( + + ))}
- {!data.allSuccess && ( + {data.apps.some((x) => !x.success) && ( {t('card.footer.error')} diff --git a/src/widgets/torrentNetworkTraffic/TorrentNetworkTrafficTile.tsx b/src/widgets/torrentNetworkTraffic/TorrentNetworkTrafficTile.tsx deleted file mode 100644 index 3b494d130..000000000 --- a/src/widgets/torrentNetworkTraffic/TorrentNetworkTrafficTile.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { NormalizedTorrent } from '@ctrl/shared-torrent'; -import { Text, Title, Group, useMantineTheme, Box, Card, ColorSwatch, Stack } from '@mantine/core'; -import { useListState } from '@mantine/hooks'; -import { showNotification } from '@mantine/notifications'; -import { linearGradientDef } from '@nivo/core'; -import { Datum, ResponsiveLine } from '@nivo/line'; -import { IconArrowsUpDown } from '@tabler/icons'; -import axios from 'axios'; -import { useTranslation } from 'next-i18next'; -import { useEffect, useState } from 'react'; -import { useConfigContext } from '../../config/provider'; -import { useSetSafeInterval } from '../../hooks/useSetSafeInterval'; -import { humanFileSize } from '../../tools/humanFileSize'; -import { NormalizedTorrentListResponse } from '../../types/api/NormalizedTorrentListResponse'; -import { defineWidget } from '../helper'; -import { IWidget } from '../widgets'; - -const definition = defineWidget({ - id: 'dlspeed', - icon: IconArrowsUpDown, - options: {}, - - gridstack: { - minWidth: 2, - minHeight: 2, - maxWidth: 12, - maxHeight: 6, - }, - component: TorrentNetworkTrafficTile, -}); - -export type ITorrentNetworkTraffic = IWidget; - -interface TorrentNetworkTrafficTileProps { - widget: ITorrentNetworkTraffic; -} - -function TorrentNetworkTrafficTile({ widget }: TorrentNetworkTrafficTileProps) { - const { t } = useTranslation(`modules/${definition.id}`); - const { colors } = useMantineTheme(); - const setSafeInterval = useSetSafeInterval(); - const { configVersion, config } = useConfigContext(); - - const [torrentHistory, torrentHistoryHandlers] = useListState([]); - const [torrents, setTorrents] = useState([]); - - const downloadServices = - config?.apps.filter( - (app) => - app.integration.type === 'qBittorrent' || - app.integration.type === 'transmission' || - app.integration.type === 'deluge' - ) ?? []; - const totalDownloadSpeed = torrents.reduce((acc, torrent) => acc + torrent.downloadSpeed, 0); - const totalUploadSpeed = torrents.reduce((acc, torrent) => acc + torrent.uploadSpeed, 0); - - useEffect(() => { - if (downloadServices.length === 0) return; - const interval = setSafeInterval(() => { - // Send one request with each download service inside - axios - .post('/api/modules/torrents') - .then((response) => { - const responseData: NormalizedTorrentListResponse = response.data; - setTorrents(responseData.torrents.flatMap((x) => x.torrents)); - }) - .catch((error) => { - if (error.status === 401) return; - setTorrents([]); - // eslint-disable-next-line no-console - console.error('Error while fetching torrents', error.response.data); - showNotification({ - title: 'Torrent speed module failed to fetch torrents', - autoClose: 1000, - disallowClose: true, - id: 'fail-torrent-speed-module', - color: 'red', - message: - 'Error fetching torrents, please check your config for any potential errors, check the console for more info', - }); - clearInterval(interval); - }); - }, 1000); - }, [configVersion]); - - useEffect(() => { - torrentHistoryHandlers.append({ - x: Date.now(), - down: totalDownloadSpeed, - up: totalUploadSpeed, - }); - }, [totalDownloadSpeed, totalUploadSpeed]); - - const history = torrentHistory.slice(-10); - const chartDataUp = history.map((load, i) => ({ - x: load.x, - y: load.up, - })) as Datum[]; - const chartDataDown = history.map((load, i) => ({ - x: load.x, - y: load.down, - })) as Datum[]; - - return ( - - {t('card.lineChart.title')} - - - - - {t('card.lineChart.totalDownload', { download: humanFileSize(totalDownloadSpeed) })} - - - - - - {t('card.lineChart.totalUpload', { upload: humanFileSize(totalUploadSpeed) })} - - - - - { - const Download = slice.points[0].data.y as number; - const Upload = slice.points[1].data.y as number; - // Get the number of seconds since the last update. - const seconds = (Date.now() - (slice.points[0].data.x as number)) / 1000; - // Round to the nearest second. - const roundedSeconds = Math.round(seconds); - return ( - - {t('card.lineChart.timeSpan', { seconds: roundedSeconds })} - - - - - - {t('card.lineChart.download', { download: humanFileSize(Download) })} - - - - - - {t('card.lineChart.upload', { upload: humanFileSize(Upload) })} - - - - - - ); - }} - data={[ - { - id: 'downloads', - data: chartDataUp, - }, - { - id: 'uploads', - data: chartDataDown, - }, - ]} - curve="monotoneX" - yFormat=" >-.2f" - axisTop={null} - axisRight={null} - enablePoints={false} - animate={false} - enableGridX={false} - enableGridY={false} - enableArea - defs={[ - linearGradientDef('gradientA', [ - { offset: 0, color: 'inherit' }, - { offset: 100, color: 'inherit', opacity: 0 }, - ]), - ]} - fill={[{ match: '*', id: 'gradientA' }]} - colors={[colors.blue[5], colors.green[5]]} - /> - - - ); -} - -export default definition; - -interface TorrentHistory { - x: number; - up: number; - down: number; -} diff --git a/src/widgets/useNet/UsenetQueueList.tsx b/src/widgets/useNet/UsenetQueueList.tsx index 5fec4f842..aa3dc99e6 100644 --- a/src/widgets/useNet/UsenetQueueList.tsx +++ b/src/widgets/useNet/UsenetQueueList.tsx @@ -1,13 +1,11 @@ import { ActionIcon, Alert, - Button, Center, Code, Group, Pagination, Progress, - ScrollArea, Skeleton, Stack, Table,