import { objectEntries } from "@homarr/common"; import { logger } from "@homarr/log"; import type { WidgetComponentProps } from "../../../widgets/src/definition"; import type { InversedWidgetMapping, OldmarrWidgetDefinitions, WidgetMapping } from "./definitions"; import { mapKind } from "./definitions"; // This type enforces, that for all widget mappings there is a corresponding option mapping, // each option of newmarr can be mapped from the value of the oldmarr options type OptionMapping = { [WidgetKey in keyof InversedWidgetMapping]: InversedWidgetMapping[WidgetKey] extends null ? null : { [OptionsKey in keyof WidgetComponentProps["options"]]: ( oldOptions: Extract["options"], appsMap: Map, ) => WidgetComponentProps["options"][OptionsKey] | undefined; }; }; const optionMapping: OptionMapping = { "mediaRequests-requestList": { linksTargetNewTab: (oldOptions) => oldOptions.openInNewTab, }, "mediaRequests-requestStats": {}, bookmarks: { title: (oldOptions) => oldOptions.name, // It's safe to assume that the app exists, because the app is always created before the widget // And the mapping is created in insertAppsAsync // eslint-disable-next-line @typescript-eslint/no-non-null-assertion items: (oldOptions, appsMap) => oldOptions.items.map((item) => appsMap.get(item.id)!), layout: (oldOptions) => { const mappedLayouts: Record["options"]["layout"]> = { autoGrid: "grid", horizontal: "row", vertical: "column", }; return mappedLayouts[oldOptions.layout]; }, hideTitle: () => undefined, hideIcon: (oldOptions) => oldOptions.items.some((item) => item.hideIcon), hideHostname: (oldOptions) => oldOptions.items.some((item) => item.hideHostname), openNewTab: (oldOptions) => oldOptions.items.some((item) => item.openNewTab), }, calendar: { releaseType: (oldOptions) => [oldOptions.radarrReleaseType], filterFutureMonths: () => undefined, filterPastMonths: () => undefined, showUnmonitored: ({ showUnmonitored }) => showUnmonitored, }, clock: { customTitle: (oldOptions) => oldOptions.customTitle, customTitleToggle: (oldOptions) => oldOptions.titleState !== "none", dateFormat: (oldOptions) => (oldOptions.dateFormat === "hide" ? undefined : oldOptions.dateFormat), is24HourFormat: (oldOptions) => oldOptions.display24HourFormat, showDate: (oldOptions) => oldOptions.dateFormat !== "hide", showSeconds: () => undefined, timezone: (oldOptions) => oldOptions.timezone, useCustomTimezone: () => true, customTimeFormat: () => undefined, customDateFormat: () => undefined, }, downloads: { activeTorrentThreshold: (oldOptions) => "speedLimitOfActiveTorrents" in oldOptions ? oldOptions.speedLimitOfActiveTorrents : undefined, applyFilterToRatio: (oldOptions) => "displayRatioWithFilter" in oldOptions ? oldOptions.displayRatioWithFilter : undefined, categoryFilter: (oldOptions) => ("labelFilter" in oldOptions ? oldOptions.labelFilter : undefined), filterIsWhitelist: (oldOptions) => "labelFilterIsWhitelist" in oldOptions ? oldOptions.labelFilterIsWhitelist : undefined, enableRowSorting: (oldOptions) => ("rowSorting" in oldOptions ? oldOptions.rowSorting : undefined), showCompletedTorrent: (oldOptions) => "displayCompletedTorrents" in oldOptions ? oldOptions.displayCompletedTorrents : undefined, columns: () => ["integration", "name", "progress", "time", "actions"], defaultSort: () => "type", descendingDefaultSort: () => false, showCompletedUsenet: () => true, showCompletedHttp: () => true, limitPerIntegration: () => undefined, }, weather: { forecastDayCount: (oldOptions) => oldOptions.forecastDays, hasForecast: (oldOptions) => oldOptions.displayWeekly, isFormatFahrenheit: (oldOptions) => oldOptions.displayInFahrenheit, disableTemperatureDecimals: () => undefined, showCurrentWindSpeed: () => undefined, location: (oldOptions) => oldOptions.location, showCity: (oldOptions) => oldOptions.displayCityName, dateFormat: (oldOptions) => (oldOptions.dateFormat === "hide" ? undefined : oldOptions.dateFormat), }, iframe: { embedUrl: (oldOptions) => oldOptions.embedUrl, allowAutoPlay: (oldOptions) => oldOptions.allowAutoPlay, allowFullScreen: (oldOptions) => oldOptions.allowFullScreen, allowPayment: (oldOptions) => oldOptions.allowPayment, allowCamera: (oldOptions) => oldOptions.allowCamera, allowMicrophone: (oldOptions) => oldOptions.allowMicrophone, allowGeolocation: (oldOptions) => oldOptions.allowGeolocation, allowScrolling: (oldOptions) => oldOptions.allowScrolling, allowTransparency: (oldOptions) => oldOptions.allowTransparency, }, video: { feedUrl: (oldOptions) => oldOptions.FeedUrl, hasAutoPlay: (oldOptions) => oldOptions.autoPlay, hasControls: (oldOptions) => oldOptions.controls, isMuted: (oldOptions) => oldOptions.muted, }, dnsHoleControls: { showToggleAllButtons: (oldOptions) => oldOptions.showToggleAllButtons, }, dnsHoleSummary: { layout: (oldOptions) => oldOptions.layout, usePiHoleColors: (oldOptions) => oldOptions.usePiHoleColors, }, rssFeed: { feedUrls: (oldOptions) => oldOptions.rssFeedUrl, enableRtl: (oldOptions) => oldOptions.enableRtl, maximumAmountPosts: (oldOptions) => oldOptions.maximumAmountOfPosts, textLinesClamp: (oldOptions) => oldOptions.textLinesClamp, }, notebook: { allowReadOnlyCheck: (oldOptions) => oldOptions.allowReadOnlyCheck, content: (oldOptions) => oldOptions.content, showToolbar: (oldOptions) => oldOptions.showToolbar, }, "smartHome-entityState": { entityId: (oldOptions) => oldOptions.entityId, displayName: (oldOptions) => oldOptions.displayName, clickable: () => undefined, entityUnit: () => undefined, }, "smartHome-executeAutomation": { automationId: (oldOptions) => oldOptions.automationId, displayName: (oldOptions) => oldOptions.displayName, }, mediaServer: { showOnlyPlaying: () => undefined, }, indexerManager: { openIndexerSiteInNewTab: (oldOptions) => oldOptions.openIndexerSiteInNewTab, }, healthMonitoring: { cpu: (oldOptions) => "cpu" in oldOptions ? oldOptions.cpu : oldOptions.graphsOrder.some((graph) => graph.key === "cpu" && graph.subValues.enabled), memory: (oldOptions) => "memory" in oldOptions ? oldOptions.memory : oldOptions.graphsOrder.some((graph) => graph.key === "ram" && graph.subValues.enabled), fahrenheit: (oldOptions) => ("fahrenheit" in oldOptions ? oldOptions.fahrenheit : undefined), fileSystem: (oldOptions) => "fileSystem" in oldOptions ? oldOptions.fileSystem : oldOptions.graphsOrder.some((graph) => graph.key === "storage" && graph.subValues.enabled), defaultTab: (oldOptions) => ("defaultTabState" in oldOptions ? oldOptions.defaultTabState : undefined), sectionIndicatorRequirement: (oldOptions) => "sectionIndicatorColor" in oldOptions ? oldOptions.sectionIndicatorColor : undefined, showUptime: () => undefined, visibleClusterSections: (oldOptions) => { if (!("showNode" in oldOptions)) return undefined; const oldKeys = { showNode: "node" as const, showLXCs: "lxc" as const, showVM: "qemu" as const, showStorage: "storage" as const, } satisfies Partial>; return objectEntries(oldKeys) .filter(([key]) => oldOptions[key]) .map(([_, section]) => section); }, }, mediaTranscoding: { defaultView: (oldOptions) => oldOptions.defaultView, queuePageSize: (oldOptions) => oldOptions.queuePageSize, }, }; /** * Maps the oldmarr options to the newmarr options * @param type old widget type * @param oldOptions oldmarr options for this item * @param appsMap map of old app ids to new app ids * @returns newmarr options for this item or null if the item did not exist in oldmarr */ export const mapOptions = ( type: K, oldOptions: Extract["options"], appsMap: Map, ) => { logger.debug(`Mapping old homarr options for widget type=${type} options=${JSON.stringify(oldOptions)}`); const kind = mapKind(type); if (!kind) { return null; } const mapping = optionMapping[kind]; return objectEntries(mapping).reduce( (acc, [key, value]: [string, (oldOptions: Record, appsMap: Map) => unknown]) => { const newValue = value(oldOptions, appsMap); logger.debug(`Mapping old homarr option kind=${kind} key=${key} newValue=${newValue as string}`); if (newValue !== undefined) { acc[key] = newValue; } return acc; }, {} as Record, ) as WidgetComponentProps>["options"]; };