Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add Tdarr integration and widget #1882

Merged
merged 18 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions public/locales/en/modules/tdarr-queue.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{
"descriptor": {
"name": "Tdarr Queue",
"description": "Displays the queue of an external Tdarr instance.",
"settings": {
"title": "Tdarr Queue Settings",
"appId": {
"label": "Select an app"
},
"defaultView": {
"label": "Default view"
},
"showHealthCheck": {
"label": "Show Health Check indicator"
},
"showHealthChecksInQueue": {
"label": "Show Health Checks in queue"
},
"queuePageSize": {
"label": "Queue: Items per page"
},
"showAppIcon": {
"label": "Show app icon in the bottom right corner"
}
}
},
"noAppSelected": "Please select an app in the widget settings",
"views": {
"workers": {
"table": {
"header": {
"name": "File",
"eta": "ETA",
"progress": "Progress"
},
"empty": "Empty"
}
},
"queue": {
"table": {
"header": {
"name": "File",
"size": "Size"
},
"footer": {
"currentIndex": "{{start}}-{{end}} of {{total}}"
},
"empty": "Empty"
}
},
"statistics": {
"empty": "Empty",
"box": {
"transcodes": "Transcodes:",
"healthChecks": "Health Checks:",
"files": "Files:",
"spaceSaved": "Saved:"
},
"pies": {
"transcodes": "Transcodes",
"healthChecks": "Health Checks",
"videoCodecs": "Codecs",
"videoContainers": "Containers",
"videoResolutions": "Resolutions"
}
}
},
"error": {
"title": "Error",
"message": "An error occurred while fetching data from Tdarr."
},
"tabs": {
"workers": "Workers",
"queue": "Queue",
"statistics": "Statistics"
},
"healthCheckStatus": {
"title": "Health Check",
"queued": "Queued",
"healthy": "Healthy",
"unhealthy": "Unhealthy"
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to be modifying this file but it's not that important.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That one slipped through the cracks while I was reverting the formatting

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import { useTranslation } from 'next-i18next';
import { highlight, languages } from 'prismjs';
import Editor from 'react-simple-code-editor';
import { useColorTheme } from '~/tools/color';
import { BackgroundImageAttachment, BackgroundImageRepeat, BackgroundImageSize } from '~/types/settings';
import {
BackgroundImageAttachment,
BackgroundImageRepeat,
BackgroundImageSize,
} from '~/types/settings';

import { useBoardCustomizationFormContext } from '../form';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,9 @@ export const availableIntegrations = [
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/home-assistant.png',
label: 'Home Assistant',
},
{
value: 'tdarr',
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/tdarr.png',
label: 'Tdarr',
},
] as const satisfies Readonly<SelectItem[]>;
50 changes: 50 additions & 0 deletions src/components/Dashboard/Tiles/Widgets/Inputs/AppSelector.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea behind the app selector is nice, but I'm not sure it's the place to do it.
First off, this is something to come already in the rework of the integration system, so we don't want to make the users used to a system to change it right after.
Second, You should not have multiple Tdarr instances. That's the whole point of tdarr and node servers. You have one master Tdarr and the nodes connect to it. If that's not the case, I'm sorry to say but you're not using it right.
You can make it autoselect the 1st one, but in any case this will be handled in the future in another way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should not be too opinionated about peoples setups, especially seeing as Homarr is an application aimed at users with a certain degree of technical prowess. There might be edge cases or situations in which two Tdarr apps might be viable.

But I will change it so the first app is preselected by default.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's why I still mentioned the first point first. Even if people should not do that, in the case they do, it'll be an option in the future.
As it is, it is preferable to not make such a change yet as it is already being worked on.
If you have such ideas that get out of the original scope, you can come to our discord and ask about it.

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Group, Select, Text } from '@mantine/core';
import { ComponentProps, forwardRef } from 'react';
import { AppAvatar } from '~/components/AppAvatar';
import { useConfigContext } from '~/config/provider';
import { IntegrationType } from '~/types/app';

export type AppSelectorProps = {
value: string;
onChange: (value: string) => void;
integrations: IntegrationType[];
selectProps?: Omit<ComponentProps<typeof Select>, 'value' | 'data' | 'onChange'>;
};

export function AppSelector(props: AppSelectorProps) {
const { value, integrations, onChange } = props;
const { config } = useConfigContext();

const apps =
config?.apps.filter(
(app) => app.integration.type && integrations.includes(app.integration.type)
) ?? [];
const selectedApp = apps.find((app) => app.id === value);

return (
<Select
value={value}
data={apps.map((app) => ({
value: app.id,
label: app.name,
}))}
onChange={onChange}
icon={selectedApp ? <AppAvatar iconUrl={selectedApp?.appearance.iconUrl} /> : undefined}
itemComponent={forwardRef(({ value, label, ...rest }, ref) => {
const app = apps.find((app) => app.id === value);

if (!app) {
return null;
}

return (
<Group ref={ref} {...rest}>
<AppAvatar iconUrl={app.appearance.iconUrl} />
<Text size="xs">{label}</Text>
</Group>
);
})}
nothingFound="No apps found"
/>
);
}
25 changes: 24 additions & 1 deletion src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ContextModalProps } from '@mantine/modals';
import { IconAlertTriangle, IconPlaylistX, IconPlus } from '@tabler/icons-react';
import { Trans, useTranslation } from 'next-i18next';
import { FC, useState } from 'react';
import { AppSelector } from '~/components/Dashboard/Tiles/Widgets/Inputs/AppSelector';
import { useConfigContext } from '~/config/provider';
import { useConfigStore } from '~/config/store';
import { mapObject } from '~/tools/client/objects';
Expand Down Expand Up @@ -85,7 +86,7 @@ export const WidgetsEditModal = ({

return (
<Stack>
{items.map(([key, _], index) => {
{items.map(([key], index) => {
const option = (currentWidgetDefinition as any).options[key] as IWidgetOptionValue;
const value = moduleProperties[key] ?? option.defaultValue;

Expand Down Expand Up @@ -385,6 +386,28 @@ const WidgetOptionTypeSwitch: FC<{
</Flex>
</Stack>
);

case 'app-select':
return (
<Stack spacing={0}>
<Group align="center" spacing="sm">
<Text size="0.875rem" weight="500">
{t(`descriptor.settings.${key}.label`)}
</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group>
<AppSelector
value={value}
onChange={(v) => handleChange(key, v ?? option.defaultValue)}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to event

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter is the new value, not an event. I'll rename it to value

integrations={option.integrations}
selectProps={{
withinPortal: true,
...option.inputProps,
}}
/>
</Stack>
);

/* eslint-enable no-case-declarations */
default:
return null;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
const handleEditClick = () => {
openContextModalGeneric<WidgetEditModalInnerProps>({
modal: 'integrationOptions',
title: <Title order={4}>{t('descriptor.settings.title')}</Title>,
title: t('descriptor.settings.title'),
SeDemal marked this conversation as resolved.
Show resolved Hide resolved
innerProps: {
widgetId: widget.id,
widgetType: integration,
Expand Down
2 changes: 2 additions & 0 deletions src/server/api/root.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { tdarrRouter } from '~/server/api/routers/tdarr';
import { createTRPCRouter } from '~/server/api/trpc';

import { appRouter } from './routers/app';
Expand Down Expand Up @@ -49,6 +50,7 @@ export const rootRouter = createTRPCRouter({
password: passwordRouter,
notebook: notebookRouter,
smartHomeEntityState: smartHomeEntityStateRouter,
tdarr: tdarrRouter,
});

// export type definition of API
Expand Down
Loading