feat: add import for config files from oldmarr (#1019)
* wip: add oldmarr config import
* wip: add support for wrong amount of categories / sections with autofix, color mapping, position adjustments of wrappers
* fix: lockfile broken
* feat: add support for form data trpc requests
* wip: improve file upload
* refactor: restructure import, add import configuration
* wip: add configurations for import to modal
* refactor: move oldmarr import to old-import package
* fix: column count not respects screen size for board
* feat: add beta badge for oldmarr config import
* chore: address pull request feedback
* fix: format issues
* fix: inconsistent versions
* fix: deepsource issues
* fix: revert {} to Record<string, never> convertion to prevent typecheck issue
* fix: inconsistent zod version
* fix: format issue
* chore: address pull request feedback
* fix: wrong import
* fix: broken lock file
* fix: inconsistent versions
* fix: format issues
This commit is contained in:
49
packages/old-import/src/fix-section-issues.ts
Normal file
49
packages/old-import/src/fix-section-issues.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { createId } from "@homarr/db";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { OldmarrConfig } from "@homarr/old-schema";
|
||||
|
||||
export const fixSectionIssues = (old: OldmarrConfig) => {
|
||||
const wrappers = old.wrappers.sort((wrapperA, wrapperB) => wrapperA.position - wrapperB.position);
|
||||
const categories = old.categories.sort((categoryA, categoryB) => categoryA.position - categoryB.position);
|
||||
|
||||
const neededSectionCount = categories.length * 2 + 1;
|
||||
const hasToMuchEmptyWrappers = wrappers.length > categories.length + 1;
|
||||
|
||||
logger.debug(
|
||||
`Fixing section issues neededSectionCount=${neededSectionCount} hasToMuchEmptyWrappers=${hasToMuchEmptyWrappers}`,
|
||||
);
|
||||
|
||||
for (let position = 0; position < neededSectionCount; position++) {
|
||||
const index = Math.floor(position / 2);
|
||||
const isEmpty = position % 2 === 0;
|
||||
const section = isEmpty ? wrappers[index] : categories[index];
|
||||
if (!section) {
|
||||
// If there are not enough empty sections for categories we need to insert them
|
||||
if (isEmpty) {
|
||||
// Insert empty wrapper for between categories
|
||||
wrappers.push({
|
||||
id: createId(),
|
||||
position,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
section.position = position;
|
||||
}
|
||||
|
||||
// Find all wrappers that should be merged into one
|
||||
const wrapperIdsToMerge = wrappers.slice(categories.length).map((section) => section.id);
|
||||
// Remove all wrappers after the first at the end
|
||||
wrappers.splice(categories.length + 1);
|
||||
|
||||
if (wrapperIdsToMerge.length >= 2) {
|
||||
logger.debug(`Found wrappers to merge count=${wrapperIdsToMerge.length}`);
|
||||
}
|
||||
|
||||
return {
|
||||
wrappers,
|
||||
categories,
|
||||
wrapperIdsToMerge,
|
||||
};
|
||||
};
|
||||
59
packages/old-import/src/import-apps.ts
Normal file
59
packages/old-import/src/import-apps.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { createId, inArray } from "@homarr/db";
|
||||
import type { Database, InferInsertModel } from "@homarr/db";
|
||||
import { apps as appsTable } from "@homarr/db/schema/sqlite";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { OldmarrApp } from "@homarr/old-schema";
|
||||
|
||||
export const insertAppsAsync = async (
|
||||
db: Database,
|
||||
apps: OldmarrApp[],
|
||||
distinctAppsByHref: boolean,
|
||||
configName: string,
|
||||
) => {
|
||||
logger.info(
|
||||
`Importing old homarr apps configuration=${configName} distinctAppsByHref=${distinctAppsByHref} apps=${apps.length}`,
|
||||
);
|
||||
const existingAppsWithHref = distinctAppsByHref
|
||||
? await db.query.apps.findMany({
|
||||
where: inArray(appsTable.href, [...new Set(apps.map((app) => app.url))]),
|
||||
})
|
||||
: [];
|
||||
|
||||
logger.debug(`Found existing apps with href count=${existingAppsWithHref.length}`);
|
||||
|
||||
const mappedApps = apps.map((app) => ({
|
||||
// Use id of existing app when it has the same href and distinctAppsByHref is true
|
||||
newId: distinctAppsByHref
|
||||
? (existingAppsWithHref.find(
|
||||
(existingApp) =>
|
||||
existingApp.href === (app.behaviour.externalUrl === "" ? app.url : app.behaviour.externalUrl) &&
|
||||
existingApp.name === app.name &&
|
||||
existingApp.iconUrl === app.appearance.iconUrl,
|
||||
)?.id ?? createId())
|
||||
: createId(),
|
||||
...app,
|
||||
}));
|
||||
|
||||
const appsToCreate = mappedApps
|
||||
.filter((app) => !existingAppsWithHref.some((existingApp) => existingApp.id === app.newId))
|
||||
.map(
|
||||
(app) =>
|
||||
({
|
||||
id: app.newId,
|
||||
name: app.name,
|
||||
iconUrl: app.appearance.iconUrl,
|
||||
href: app.behaviour.externalUrl === "" ? app.url : app.behaviour.externalUrl,
|
||||
description: app.behaviour.tooltipDescription,
|
||||
}) satisfies InferInsertModel<typeof appsTable>,
|
||||
);
|
||||
|
||||
logger.debug(`Creating apps count=${appsToCreate.length}`);
|
||||
|
||||
if (appsToCreate.length > 0) {
|
||||
await db.insert(appsTable).values(appsToCreate);
|
||||
}
|
||||
|
||||
logger.info(`Imported apps count=${appsToCreate.length}`);
|
||||
|
||||
return mappedApps;
|
||||
};
|
||||
35
packages/old-import/src/import-board.ts
Normal file
35
packages/old-import/src/import-board.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { Database } from "@homarr/db";
|
||||
import { createId } from "@homarr/db";
|
||||
import { boards } from "@homarr/db/schema/sqlite";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { OldmarrConfig } from "@homarr/old-schema";
|
||||
import type { OldmarrImportConfiguration } from "@homarr/validation";
|
||||
|
||||
import { mapColor } from "./mappers/map-colors";
|
||||
import { mapColumnCount } from "./mappers/map-column-count";
|
||||
|
||||
export const insertBoardAsync = async (db: Database, old: OldmarrConfig, configuration: OldmarrImportConfiguration) => {
|
||||
logger.info(`Importing old homarr board configuration=${old.configProperties.name}`);
|
||||
const boardId = createId();
|
||||
await db.insert(boards).values({
|
||||
id: boardId,
|
||||
name: configuration.name,
|
||||
backgroundImageAttachment: old.settings.customization.backgroundImageAttachment,
|
||||
backgroundImageUrl: old.settings.customization.backgroundImageUrl,
|
||||
backgroundImageRepeat: old.settings.customization.backgroundImageRepeat,
|
||||
backgroundImageSize: old.settings.customization.backgroundImageSize,
|
||||
columnCount: mapColumnCount(old, configuration.screenSize),
|
||||
faviconImageUrl: old.settings.customization.faviconUrl,
|
||||
isPublic: old.settings.access.allowGuests,
|
||||
logoImageUrl: old.settings.customization.logoImageUrl,
|
||||
pageTitle: old.settings.customization.pageTitle,
|
||||
metaTitle: old.settings.customization.metaTitle,
|
||||
opacity: old.settings.customization.appOpacity,
|
||||
primaryColor: mapColor(old.settings.customization.colors.primary, "#fa5252"),
|
||||
secondaryColor: mapColor(old.settings.customization.colors.secondary, "#fd7e14"),
|
||||
});
|
||||
|
||||
logger.info(`Imported board id=${boardId}`);
|
||||
|
||||
return boardId;
|
||||
};
|
||||
16
packages/old-import/src/import-error.ts
Normal file
16
packages/old-import/src/import-error.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { OldmarrConfig } from "@homarr/old-schema";
|
||||
import type { OldmarrImportConfiguration } from "@homarr/validation";
|
||||
|
||||
export class OldHomarrImportError extends Error {
|
||||
constructor(oldConfig: OldmarrConfig, cause: unknown) {
|
||||
super(`Failed to import old homarr configuration name=${oldConfig.configProperties.name}`, {
|
||||
cause,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class OldHomarrScreenSizeError extends Error {
|
||||
constructor(type: "app" | "widget", id: string, screenSize: OldmarrImportConfiguration["screenSize"]) {
|
||||
super(`Screen size not found for type=${type} id=${id} screenSize=${screenSize}`);
|
||||
}
|
||||
}
|
||||
98
packages/old-import/src/import-items.ts
Normal file
98
packages/old-import/src/import-items.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
import type { Database } from "@homarr/db";
|
||||
import { createId } from "@homarr/db";
|
||||
import { items } from "@homarr/db/schema/sqlite";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { OldmarrApp, OldmarrWidget } from "@homarr/old-schema";
|
||||
import type { OldmarrImportConfiguration } from "@homarr/validation";
|
||||
|
||||
import type { WidgetComponentProps } from "../../widgets/src/definition";
|
||||
import { OldHomarrScreenSizeError } from "./import-error";
|
||||
import { mapKind } from "./widgets/definitions";
|
||||
import { mapOptions } from "./widgets/options";
|
||||
|
||||
export const insertItemsAsync = async (
|
||||
db: Database,
|
||||
widgets: OldmarrWidget[],
|
||||
mappedApps: (OldmarrApp & { newId: string })[],
|
||||
sectionIdMaps: Map<string, string>,
|
||||
configuration: OldmarrImportConfiguration,
|
||||
) => {
|
||||
logger.info(`Importing old homarr items widgets=${widgets.length} apps=${mappedApps.length}`);
|
||||
|
||||
for (const widget of widgets) {
|
||||
// All items should have been moved to the last wrapper
|
||||
if (widget.area.type === "sidebar") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const kind = mapKind(widget.type);
|
||||
|
||||
logger.debug(`Mapped widget kind id=${widget.id} previous=${widget.type} current=${kind}`);
|
||||
|
||||
if (!kind) {
|
||||
logger.error(`Widget has no kind id=${widget.id} type=${widget.type}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const sectionId = sectionIdMaps.get(widget.area.properties.id)!;
|
||||
|
||||
logger.debug(`Inserting widget id=${widget.id} sectionId=${sectionId}`);
|
||||
|
||||
const screenSizeShape = widget.shape[configuration.screenSize];
|
||||
if (!screenSizeShape) {
|
||||
throw new OldHomarrScreenSizeError("widget", widget.id, configuration.screenSize);
|
||||
}
|
||||
|
||||
await db.insert(items).values({
|
||||
id: createId(),
|
||||
sectionId,
|
||||
height: screenSizeShape.size.height,
|
||||
width: screenSizeShape.size.width,
|
||||
xOffset: screenSizeShape.location.x,
|
||||
yOffset: screenSizeShape.location.y,
|
||||
kind,
|
||||
options: SuperJSON.stringify(mapOptions(kind, widget.properties)),
|
||||
});
|
||||
|
||||
logger.debug(`Inserted widget id=${widget.id} sectionId=${sectionId}`);
|
||||
}
|
||||
|
||||
for (const app of mappedApps) {
|
||||
// All items should have been moved to the last wrapper
|
||||
if (app.area.type === "sidebar") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const sectionId = sectionIdMaps.get(app.area.properties.id)!;
|
||||
|
||||
logger.debug(`Inserting app name=${app.name} sectionId=${sectionId}`);
|
||||
|
||||
const screenSizeShape = app.shape[configuration.screenSize];
|
||||
if (!screenSizeShape) {
|
||||
throw new OldHomarrScreenSizeError("app", app.id, configuration.screenSize);
|
||||
}
|
||||
|
||||
await db.insert(items).values({
|
||||
id: createId(),
|
||||
sectionId,
|
||||
height: screenSizeShape.size.height,
|
||||
width: screenSizeShape.size.width,
|
||||
xOffset: screenSizeShape.location.x,
|
||||
yOffset: screenSizeShape.location.y,
|
||||
kind: "app",
|
||||
options: SuperJSON.stringify({
|
||||
appId: app.newId,
|
||||
openInNewTab: app.behaviour.isOpeningNewTab,
|
||||
pingEnabled: app.network.enabledStatusChecker,
|
||||
showDescriptionTooltip: app.behaviour.tooltipDescription !== "",
|
||||
showTitle: app.appearance.appNameStatus === "normal",
|
||||
} satisfies WidgetComponentProps<"app">["options"]),
|
||||
});
|
||||
|
||||
logger.debug(`Inserted app name=${app.name} sectionId=${sectionId}`);
|
||||
}
|
||||
};
|
||||
51
packages/old-import/src/import-sections.ts
Normal file
51
packages/old-import/src/import-sections.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { createId } from "@homarr/db";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { sections } from "@homarr/db/schema/sqlite";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { OldmarrConfig } from "@homarr/old-schema";
|
||||
|
||||
export const insertSectionsAsync = async (
|
||||
db: Database,
|
||||
categories: OldmarrConfig["categories"],
|
||||
wrappers: OldmarrConfig["wrappers"],
|
||||
boardId: string,
|
||||
) => {
|
||||
logger.info(
|
||||
`Importing old homarr sections boardId=${boardId} categories=${categories.length} wrappers=${wrappers.length}`,
|
||||
);
|
||||
|
||||
const wrapperIds = wrappers.map((section) => section.id);
|
||||
const categoryIds = categories.map((section) => section.id);
|
||||
const idMaps = new Map<string, string>([...wrapperIds, ...categoryIds].map((id) => [id, createId()]));
|
||||
|
||||
const wrappersToInsert = wrappers.map((section) => ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
id: idMaps.get(section.id)!,
|
||||
boardId,
|
||||
xOffset: 0,
|
||||
yOffset: section.position,
|
||||
kind: "empty" as const,
|
||||
}));
|
||||
|
||||
const categoriesToInsert = categories.map((section) => ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
id: idMaps.get(section.id)!,
|
||||
boardId,
|
||||
xOffset: 0,
|
||||
yOffset: section.position,
|
||||
kind: "category" as const,
|
||||
name: section.name,
|
||||
}));
|
||||
|
||||
if (wrappersToInsert.length > 0) {
|
||||
await db.insert(sections).values(wrappersToInsert);
|
||||
}
|
||||
|
||||
if (categoriesToInsert.length > 0) {
|
||||
await db.insert(sections).values(categoriesToInsert);
|
||||
}
|
||||
|
||||
logger.info(`Imported sections count=${wrappersToInsert.length + categoriesToInsert.length}`);
|
||||
|
||||
return idMaps;
|
||||
};
|
||||
47
packages/old-import/src/index.ts
Normal file
47
packages/old-import/src/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { Database } from "@homarr/db";
|
||||
import type { OldmarrConfig } from "@homarr/old-schema";
|
||||
import type { OldmarrImportConfiguration } from "@homarr/validation";
|
||||
|
||||
import { fixSectionIssues } from "./fix-section-issues";
|
||||
import { insertAppsAsync } from "./import-apps";
|
||||
import { insertBoardAsync } from "./import-board";
|
||||
import { OldHomarrImportError, OldHomarrScreenSizeError } from "./import-error";
|
||||
import { insertItemsAsync } from "./import-items";
|
||||
import { insertSectionsAsync } from "./import-sections";
|
||||
import { moveWidgetsAndAppsIfMerge } from "./move-widgets-and-apps-merge";
|
||||
|
||||
export const importAsync = async (db: Database, old: OldmarrConfig, configuration: OldmarrImportConfiguration) => {
|
||||
if (configuration.onlyImportApps) {
|
||||
await db
|
||||
.transaction(async (trasaction) => {
|
||||
await insertAppsAsync(trasaction, old.apps, configuration.distinctAppsByHref, old.configProperties.name);
|
||||
})
|
||||
.catch((error) => {
|
||||
throw new OldHomarrImportError(old, error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await db
|
||||
.transaction(async (trasaction) => {
|
||||
const { wrappers, categories, wrapperIdsToMerge } = fixSectionIssues(old);
|
||||
const { apps, widgets } = moveWidgetsAndAppsIfMerge(old, wrapperIdsToMerge, configuration);
|
||||
|
||||
const boardId = await insertBoardAsync(trasaction, old, configuration);
|
||||
const sectionIdMaps = await insertSectionsAsync(trasaction, categories, wrappers, boardId);
|
||||
const mappedApps = await insertAppsAsync(
|
||||
trasaction,
|
||||
apps,
|
||||
configuration.distinctAppsByHref,
|
||||
old.configProperties.name,
|
||||
);
|
||||
await insertItemsAsync(trasaction, widgets, mappedApps, sectionIdMaps, configuration);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error instanceof OldHomarrScreenSizeError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new OldHomarrImportError(old, error);
|
||||
});
|
||||
};
|
||||
48
packages/old-import/src/mappers/map-colors.ts
Normal file
48
packages/old-import/src/mappers/map-colors.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
const oldColors = [
|
||||
"dark",
|
||||
"gray",
|
||||
"red",
|
||||
"pink",
|
||||
"grape",
|
||||
"violet",
|
||||
"indigo",
|
||||
"blue",
|
||||
"cyan",
|
||||
"green",
|
||||
"lime",
|
||||
"yellow",
|
||||
"orange",
|
||||
"teal",
|
||||
] as const;
|
||||
type OldColor = (typeof oldColors)[number];
|
||||
|
||||
export const mapColor = (color: string | undefined, fallback: string) => {
|
||||
if (!color) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (!oldColors.some((mantineColor) => color === mantineColor)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const mantineColor = color as OldColor;
|
||||
|
||||
return mappedColors[mantineColor];
|
||||
};
|
||||
|
||||
const mappedColors: Record<(typeof oldColors)[number], string> = {
|
||||
blue: "#228be6",
|
||||
cyan: "#15aabf",
|
||||
dark: "#2e2e2e",
|
||||
grape: "#be4bdb",
|
||||
gray: "#868e96",
|
||||
green: "#40c057",
|
||||
indigo: "#4c6ef5",
|
||||
lime: "#82c91e",
|
||||
orange: "#fd7e14",
|
||||
pink: "#e64980",
|
||||
red: "#fa5252",
|
||||
teal: "#12b886",
|
||||
violet: "#7950f2",
|
||||
yellow: "#fab005",
|
||||
};
|
||||
15
packages/old-import/src/mappers/map-column-count.ts
Normal file
15
packages/old-import/src/mappers/map-column-count.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { OldmarrConfig } from "@homarr/old-schema";
|
||||
import type { OldmarrImportConfiguration } from "@homarr/validation";
|
||||
|
||||
export const mapColumnCount = (old: OldmarrConfig, screenSize: OldmarrImportConfiguration["screenSize"]) => {
|
||||
switch (screenSize) {
|
||||
case "lg":
|
||||
return old.settings.customization.gridstack.columnCountLarge;
|
||||
case "md":
|
||||
return old.settings.customization.gridstack.columnCountMedium;
|
||||
case "sm":
|
||||
return old.settings.customization.gridstack.columnCountSmall;
|
||||
default:
|
||||
return 10;
|
||||
}
|
||||
};
|
||||
300
packages/old-import/src/move-widgets-and-apps-merge.ts
Normal file
300
packages/old-import/src/move-widgets-and-apps-merge.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
import { objectEntries } from "@homarr/common";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { OldmarrApp, OldmarrConfig, OldmarrWidget } from "@homarr/old-schema";
|
||||
import type { OldmarrImportConfiguration } from "@homarr/validation";
|
||||
|
||||
import { OldHomarrScreenSizeError } from "./import-error";
|
||||
import { mapColumnCount } from "./mappers/map-column-count";
|
||||
|
||||
export const moveWidgetsAndAppsIfMerge = (
|
||||
old: OldmarrConfig,
|
||||
wrapperIdsToMerge: string[],
|
||||
configuration: OldmarrImportConfiguration,
|
||||
) => {
|
||||
const firstId = wrapperIdsToMerge[0];
|
||||
if (!firstId) {
|
||||
return { apps: old.apps, widgets: old.widgets };
|
||||
}
|
||||
|
||||
const affectedMap = new Map<string, { apps: OldmarrApp[]; widgets: OldmarrWidget[] }>(
|
||||
wrapperIdsToMerge.map((id) => [
|
||||
id,
|
||||
{
|
||||
apps: old.apps.filter((app) => app.area.type !== "sidebar" && id === app.area.properties.id),
|
||||
widgets: old.widgets.filter((app) => app.area.type !== "sidebar" && id === app.area.properties.id),
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
logger.debug(`Merging wrappers at the end of the board count=${wrapperIdsToMerge.length}`);
|
||||
|
||||
let offset = 0;
|
||||
for (const id of wrapperIdsToMerge) {
|
||||
let requiredHeight = 0;
|
||||
const affected = affectedMap.get(id);
|
||||
if (!affected) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const apps = affected.apps;
|
||||
const widgets = affected.widgets;
|
||||
|
||||
for (const app of apps) {
|
||||
if (app.area.type === "sidebar") continue;
|
||||
// Move item to first wrapper
|
||||
app.area.properties.id = firstId;
|
||||
|
||||
const screenSizeShape = app.shape[configuration.screenSize];
|
||||
if (!screenSizeShape) {
|
||||
throw new OldHomarrScreenSizeError("app", app.id, configuration.screenSize);
|
||||
}
|
||||
|
||||
// Find the highest widget in the wrapper to increase the offset accordingly
|
||||
if (screenSizeShape.location.y + screenSizeShape.size.height > requiredHeight) {
|
||||
requiredHeight = screenSizeShape.location.y + screenSizeShape.size.height;
|
||||
}
|
||||
|
||||
// Move item down as much as needed to not overlap with other items
|
||||
screenSizeShape.location.y += offset;
|
||||
}
|
||||
|
||||
for (const widget of widgets) {
|
||||
if (widget.area.type === "sidebar") continue;
|
||||
// Move item to first wrapper
|
||||
widget.area.properties.id = firstId;
|
||||
|
||||
const screenSizeShape = widget.shape[configuration.screenSize];
|
||||
if (!screenSizeShape) {
|
||||
throw new OldHomarrScreenSizeError("widget", widget.id, configuration.screenSize);
|
||||
}
|
||||
|
||||
// Find the highest widget in the wrapper to increase the offset accordingly
|
||||
if (screenSizeShape.location.y + screenSizeShape.size.height > requiredHeight) {
|
||||
requiredHeight = screenSizeShape.location.y + screenSizeShape.size.height;
|
||||
}
|
||||
|
||||
// Move item down as much as needed to not overlap with other items
|
||||
screenSizeShape.location.y += offset;
|
||||
}
|
||||
|
||||
offset += requiredHeight;
|
||||
}
|
||||
|
||||
if (configuration.sidebarBehaviour === "last-section") {
|
||||
if (old.settings.customization.layout.enabledLeftSidebar) {
|
||||
offset = moveWidgetsAndAppsInLeftSidebar(old, firstId, offset, configuration.screenSize);
|
||||
}
|
||||
|
||||
if (old.settings.customization.layout.enabledRightSidebar) {
|
||||
moveWidgetsAndAppsInRightSidebar(old, firstId, offset, configuration.screenSize);
|
||||
}
|
||||
}
|
||||
|
||||
return { apps: old.apps, widgets: old.widgets };
|
||||
};
|
||||
|
||||
const moveWidgetsAndAppsInLeftSidebar = (
|
||||
old: OldmarrConfig,
|
||||
firstId: string,
|
||||
offset: number,
|
||||
screenSize: OldmarrImportConfiguration["screenSize"],
|
||||
) => {
|
||||
const columnCount = mapColumnCount(old, screenSize);
|
||||
let requiredHeight = updateItems({
|
||||
// This should work as the reference of the items did not change, only the array reference did
|
||||
items: [...old.widgets, ...old.apps],
|
||||
screenSize,
|
||||
filter: (item) =>
|
||||
item.area.type === "sidebar" &&
|
||||
item.area.properties.location === "left" &&
|
||||
(columnCount >= 2 || item.shape[screenSize]?.location.x === 0),
|
||||
update: (item) => {
|
||||
const screenSizeShape = item.shape[screenSize];
|
||||
if (!screenSizeShape) {
|
||||
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize);
|
||||
}
|
||||
// Reduce width to one if column count is one
|
||||
if (screenSizeShape.size.width > columnCount) {
|
||||
screenSizeShape.size.width = columnCount;
|
||||
}
|
||||
|
||||
item.area = {
|
||||
type: "wrapper",
|
||||
properties: {
|
||||
id: firstId,
|
||||
},
|
||||
};
|
||||
|
||||
screenSizeShape.location.y += offset;
|
||||
},
|
||||
});
|
||||
|
||||
// Only increase offset if there are less than 3 columns because then the items have to be stacked
|
||||
if (columnCount <= 3) {
|
||||
offset += requiredHeight;
|
||||
}
|
||||
|
||||
// When column count is 0 we need to stack the items of the sidebar on top of each other
|
||||
if (columnCount !== 1) {
|
||||
return offset;
|
||||
}
|
||||
|
||||
requiredHeight = updateItems({
|
||||
// This should work as the reference of the items did not change, only the array reference did
|
||||
items: [...old.widgets, ...old.apps],
|
||||
screenSize,
|
||||
filter: (item) =>
|
||||
item.area.type === "sidebar" &&
|
||||
item.area.properties.location === "left" &&
|
||||
item.shape[screenSize]?.location.x === 1,
|
||||
update: (item) => {
|
||||
const screenSizeShape = item.shape[screenSize];
|
||||
if (!screenSizeShape) {
|
||||
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize);
|
||||
}
|
||||
|
||||
item.area = {
|
||||
type: "wrapper",
|
||||
properties: {
|
||||
id: firstId,
|
||||
},
|
||||
};
|
||||
|
||||
screenSizeShape.location.x = 0;
|
||||
screenSizeShape.location.y += offset;
|
||||
},
|
||||
});
|
||||
|
||||
offset += requiredHeight;
|
||||
return offset;
|
||||
};
|
||||
|
||||
const moveWidgetsAndAppsInRightSidebar = (
|
||||
old: OldmarrConfig,
|
||||
firstId: string,
|
||||
offset: number,
|
||||
screenSize: OldmarrImportConfiguration["screenSize"],
|
||||
) => {
|
||||
const columnCount = mapColumnCount(old, screenSize);
|
||||
const xOffsetDelta = Math.max(columnCount - 2, 0);
|
||||
const requiredHeight = updateItems({
|
||||
// This should work as the reference of the items did not change, only the array reference did
|
||||
items: [...old.widgets, ...old.apps],
|
||||
screenSize,
|
||||
filter: (item) =>
|
||||
item.area.type === "sidebar" &&
|
||||
item.area.properties.location === "right" &&
|
||||
(columnCount >= 2 || item.shape[screenSize]?.location.x === 0),
|
||||
update: (item) => {
|
||||
const screenSizeShape = item.shape[screenSize];
|
||||
if (!screenSizeShape) {
|
||||
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize);
|
||||
}
|
||||
|
||||
// Reduce width to one if column count is one
|
||||
if (screenSizeShape.size.width > columnCount) {
|
||||
screenSizeShape.size.width = columnCount;
|
||||
}
|
||||
|
||||
item.area = {
|
||||
type: "wrapper",
|
||||
properties: {
|
||||
id: firstId,
|
||||
},
|
||||
};
|
||||
|
||||
screenSizeShape.location.y += offset;
|
||||
screenSizeShape.location.x += xOffsetDelta;
|
||||
},
|
||||
});
|
||||
|
||||
// When column count is 0 we need to stack the items of the sidebar on top of each other
|
||||
if (columnCount !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
offset += requiredHeight;
|
||||
|
||||
updateItems({
|
||||
// This should work as the reference of the items did not change, only the array reference did
|
||||
items: [...old.widgets, ...old.apps],
|
||||
screenSize,
|
||||
filter: (item) =>
|
||||
item.area.type === "sidebar" &&
|
||||
item.area.properties.location === "left" &&
|
||||
item.shape[screenSize]?.location.x === 1,
|
||||
update: (item) => {
|
||||
const screenSizeShape = item.shape[screenSize];
|
||||
if (!screenSizeShape) {
|
||||
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize);
|
||||
}
|
||||
|
||||
item.area = {
|
||||
type: "wrapper",
|
||||
properties: {
|
||||
id: firstId,
|
||||
},
|
||||
};
|
||||
|
||||
screenSizeShape.location.x = 0;
|
||||
screenSizeShape.location.y += offset;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createItemSnapshot = (
|
||||
item: OldmarrApp | OldmarrWidget,
|
||||
screenSize: OldmarrImportConfiguration["screenSize"],
|
||||
) => ({
|
||||
x: item.shape[screenSize]?.location.x,
|
||||
y: item.shape[screenSize]?.location.y,
|
||||
height: item.shape[screenSize]?.size.height,
|
||||
width: item.shape[screenSize]?.size.width,
|
||||
section:
|
||||
item.area.type === "sidebar"
|
||||
? {
|
||||
type: "sidebar",
|
||||
location: item.area.properties.location,
|
||||
}
|
||||
: {
|
||||
type: item.area.type,
|
||||
id: item.area.properties.id,
|
||||
},
|
||||
toString(): string {
|
||||
return objectEntries(this)
|
||||
.filter(([key]) => key !== "toString")
|
||||
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
|
||||
.join(" ");
|
||||
},
|
||||
});
|
||||
|
||||
const updateItems = (options: {
|
||||
items: (OldmarrApp | OldmarrWidget)[];
|
||||
filter: (item: OldmarrApp | OldmarrWidget) => boolean;
|
||||
update: (item: OldmarrApp | OldmarrWidget) => void;
|
||||
screenSize: OldmarrImportConfiguration["screenSize"];
|
||||
}) => {
|
||||
const items = options.items.filter(options.filter);
|
||||
let requiredHeight = 0;
|
||||
for (const item of items) {
|
||||
const before = createItemSnapshot(item, options.screenSize);
|
||||
|
||||
const screenSizeShape = item.shape[options.screenSize];
|
||||
if (!screenSizeShape) {
|
||||
throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, options.screenSize);
|
||||
}
|
||||
|
||||
if (screenSizeShape.location.y + screenSizeShape.size.height > requiredHeight) {
|
||||
requiredHeight = screenSizeShape.location.y + screenSizeShape.size.height;
|
||||
}
|
||||
|
||||
options.update(item);
|
||||
const after = createItemSnapshot(item, options.screenSize);
|
||||
|
||||
logger.debug(
|
||||
`Moved item ${item.id}\n [snapshot before]: ${before.toString()}\n [snapshot after]: ${after.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
return requiredHeight;
|
||||
};
|
||||
18
packages/old-import/src/widgets/definitions/bookmark.ts
Normal file
18
packages/old-import/src/widgets/definitions/bookmark.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrBookmarkDefinition = CommonOldmarrWidgetDefinition<
|
||||
"bookmark",
|
||||
{
|
||||
name: string;
|
||||
items: {
|
||||
id: string;
|
||||
name: string;
|
||||
href: string;
|
||||
iconUrl: string;
|
||||
openNewTab: boolean;
|
||||
hideHostname: boolean;
|
||||
hideIcon: boolean;
|
||||
}[];
|
||||
layout: "autoGrid" | "horizontal" | "vertical";
|
||||
}
|
||||
>;
|
||||
11
packages/old-import/src/widgets/definitions/calendar.ts
Normal file
11
packages/old-import/src/widgets/definitions/calendar.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrCalendarDefinition = CommonOldmarrWidgetDefinition<
|
||||
"calendar",
|
||||
{
|
||||
hideWeekDays: boolean;
|
||||
showUnmonitored: boolean;
|
||||
radarrReleaseType: "inCinemas" | "physicalRelease" | "digitalRelease";
|
||||
fontSize: "xs" | "sm" | "md" | "lg" | "xl";
|
||||
}
|
||||
>;
|
||||
9
packages/old-import/src/widgets/definitions/common.ts
Normal file
9
packages/old-import/src/widgets/definitions/common.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { OldmarrWidgetKind } from "@homarr/old-schema";
|
||||
|
||||
export interface CommonOldmarrWidgetDefinition<
|
||||
TId extends OldmarrWidgetKind,
|
||||
TOptions extends Record<string, unknown>,
|
||||
> {
|
||||
id: TId;
|
||||
options: TOptions;
|
||||
}
|
||||
53
packages/old-import/src/widgets/definitions/dashdot.ts
Normal file
53
packages/old-import/src/widgets/definitions/dashdot.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrDashdotDefinition = CommonOldmarrWidgetDefinition<
|
||||
"dashdot",
|
||||
{
|
||||
dashName: string;
|
||||
url: string;
|
||||
usePercentages: boolean;
|
||||
columns: number;
|
||||
graphHeight: number;
|
||||
graphsOrder: (
|
||||
| {
|
||||
key: "storage";
|
||||
subValues: {
|
||||
enabled: boolean;
|
||||
compactView: boolean;
|
||||
span: number;
|
||||
multiView: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
key: "network";
|
||||
subValues: {
|
||||
enabled: boolean;
|
||||
compactView: boolean;
|
||||
span: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
key: "cpu";
|
||||
subValues: {
|
||||
enabled: boolean;
|
||||
multiView: boolean;
|
||||
span: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
key: "ram";
|
||||
subValues: {
|
||||
enabled: boolean;
|
||||
span: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
key: "gpu";
|
||||
subValues: {
|
||||
enabled: boolean;
|
||||
span: number;
|
||||
};
|
||||
}
|
||||
)[];
|
||||
}
|
||||
>;
|
||||
21
packages/old-import/src/widgets/definitions/date.ts
Normal file
21
packages/old-import/src/widgets/definitions/date.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrDateDefinition = CommonOldmarrWidgetDefinition<
|
||||
"date",
|
||||
{
|
||||
timezone: string;
|
||||
customTitle: string;
|
||||
display24HourFormat: boolean;
|
||||
dateFormat:
|
||||
| "hide"
|
||||
| "dddd, MMMM D"
|
||||
| "dddd, D MMMM"
|
||||
| "MMM D"
|
||||
| "D MMM"
|
||||
| "DD/MM/YYYY"
|
||||
| "MM/DD/YYYY"
|
||||
| "DD/MM"
|
||||
| "MM/DD";
|
||||
titleState: "none" | "city" | "both";
|
||||
}
|
||||
>;
|
||||
4
packages/old-import/src/widgets/definitions/dlspeed.ts
Normal file
4
packages/old-import/src/widgets/definitions/dlspeed.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export type OldmarrDlspeedDefinition = CommonOldmarrWidgetDefinition<"dlspeed", {}>;
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrDnsHoleControlsDefinition = CommonOldmarrWidgetDefinition<
|
||||
"dns-hole-controls",
|
||||
{
|
||||
showToggleAllButtons: boolean;
|
||||
}
|
||||
>;
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrDnsHoleSummaryDefinition = CommonOldmarrWidgetDefinition<
|
||||
"dns-hole-summary",
|
||||
{ usePiHoleColors: boolean; layout: "column" | "row" | "grid" }
|
||||
>;
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrHealthMonitoringDefinition = CommonOldmarrWidgetDefinition<
|
||||
"health-monitoring",
|
||||
{
|
||||
fahrenheit: boolean;
|
||||
cpu: boolean;
|
||||
memory: boolean;
|
||||
fileSystem: boolean;
|
||||
defaultTabState: "system" | "cluster";
|
||||
node: string;
|
||||
defaultViewState: "storage" | "none" | "node" | "vm" | "lxc";
|
||||
summary: boolean;
|
||||
showNode: boolean;
|
||||
showVM: boolean;
|
||||
showLXCs: boolean;
|
||||
showStorage: boolean;
|
||||
sectionIndicatorColor: "all" | "any";
|
||||
ignoreCert: boolean;
|
||||
}
|
||||
>;
|
||||
16
packages/old-import/src/widgets/definitions/iframe.ts
Normal file
16
packages/old-import/src/widgets/definitions/iframe.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrIframeDefinition = CommonOldmarrWidgetDefinition<
|
||||
"iframe",
|
||||
{
|
||||
embedUrl: string;
|
||||
allowFullScreen: boolean;
|
||||
allowScrolling: boolean;
|
||||
allowTransparency: boolean;
|
||||
allowPayment: boolean;
|
||||
allowAutoPlay: boolean;
|
||||
allowMicrophone: boolean;
|
||||
allowCamera: boolean;
|
||||
allowGeolocation: boolean;
|
||||
}
|
||||
>;
|
||||
75
packages/old-import/src/widgets/definitions/index.ts
Normal file
75
packages/old-import/src/widgets/definitions/index.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { objectEntries } from "@homarr/common";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
|
||||
import type { OldmarrBookmarkDefinition } from "./bookmark";
|
||||
import type { OldmarrCalendarDefinition } from "./calendar";
|
||||
import type { OldmarrDashdotDefinition } from "./dashdot";
|
||||
import type { OldmarrDateDefinition } from "./date";
|
||||
import type { OldmarrDlspeedDefinition } from "./dlspeed";
|
||||
import type { OldmarrDnsHoleControlsDefinition } from "./dns-hole-controls";
|
||||
import type { OldmarrDnsHoleSummaryDefinition } from "./dns-hole-summary";
|
||||
import type { OldmarrHealthMonitoringDefinition } from "./health-monitoring";
|
||||
import type { OldmarrIframeDefinition } from "./iframe";
|
||||
import type { OldmarrIndexerManagerDefinition } from "./indexer-manager";
|
||||
import type { OldmarrMediaRequestListDefinition } from "./media-requests-list";
|
||||
import type { OldmarrMediaRequestStatsDefinition } from "./media-requests-stats";
|
||||
import type { OldmarrMediaServerDefinition } from "./media-server";
|
||||
import type { OldmarrMediaTranscodingDefinition } from "./media-transcoding";
|
||||
import type { OldmarrNotebookDefinition } from "./notebook";
|
||||
import type { OldmarrRssDefinition } from "./rss";
|
||||
import type { OldmarrSmartHomeEntityStateDefinition } from "./smart-home-entity-state";
|
||||
import type { OldmarrSmartHomeTriggerAutomationDefinition } from "./smart-home-trigger-automation";
|
||||
import type { OldmarrTorrentStatusDefinition } from "./torrent-status";
|
||||
import type { OldmarrUsenetDefinition } from "./usenet";
|
||||
import type { OldmarrVideoStreamDefinition } from "./video-stream";
|
||||
import type { OldmarrWeatherDefinition } from "./weather";
|
||||
|
||||
export type OldmarrWidgetDefinitions =
|
||||
| OldmarrWeatherDefinition
|
||||
| OldmarrDateDefinition
|
||||
| OldmarrCalendarDefinition
|
||||
| OldmarrIndexerManagerDefinition
|
||||
| OldmarrDashdotDefinition
|
||||
| OldmarrUsenetDefinition
|
||||
| OldmarrTorrentStatusDefinition
|
||||
| OldmarrDlspeedDefinition
|
||||
| OldmarrRssDefinition
|
||||
| OldmarrVideoStreamDefinition
|
||||
| OldmarrIframeDefinition
|
||||
| OldmarrMediaServerDefinition
|
||||
| OldmarrMediaRequestListDefinition
|
||||
| OldmarrMediaRequestStatsDefinition
|
||||
| OldmarrDnsHoleSummaryDefinition
|
||||
| OldmarrDnsHoleControlsDefinition
|
||||
| OldmarrBookmarkDefinition
|
||||
| OldmarrNotebookDefinition
|
||||
| OldmarrSmartHomeEntityStateDefinition
|
||||
| OldmarrSmartHomeTriggerAutomationDefinition
|
||||
| OldmarrHealthMonitoringDefinition
|
||||
| OldmarrMediaTranscodingDefinition;
|
||||
|
||||
export const widgetKindMapping = {
|
||||
app: null, // In oldmarr apps were not widgets
|
||||
clock: "date",
|
||||
calendar: "calendar",
|
||||
weather: "weather",
|
||||
rssFeed: "rss",
|
||||
video: "video-stream",
|
||||
iframe: "iframe",
|
||||
mediaServer: "media-server",
|
||||
dnsHoleSummary: "dns-hole-summary",
|
||||
dnsHoleControls: "dns-hole-controls",
|
||||
notebook: "notebook",
|
||||
"smartHome-entityState": "smart-home/entity-state",
|
||||
"smartHome-executeAutomation": "smart-home/trigger-automation",
|
||||
"mediaRequests-requestList": "media-requests-list",
|
||||
"mediaRequests-requestStats": "media-requests-stats",
|
||||
} satisfies Record<WidgetKind, OldmarrWidgetDefinitions["id"] | null>;
|
||||
// Use null for widgets that did not exist in oldmarr
|
||||
// TODO: revert assignment so that only old widgets are needed in the object,
|
||||
// this can be done ones all widgets are implemented
|
||||
|
||||
export type WidgetMapping = typeof widgetKindMapping;
|
||||
|
||||
export const mapKind = (kind: OldmarrWidgetDefinitions["id"]): WidgetKind | undefined =>
|
||||
objectEntries(widgetKindMapping).find(([_, value]) => value === kind)?.[0];
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrIndexerManagerDefinition = CommonOldmarrWidgetDefinition<
|
||||
"indexer-manager",
|
||||
{
|
||||
openIndexerSiteInNewTab: boolean;
|
||||
}
|
||||
>;
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrMediaRequestListDefinition = CommonOldmarrWidgetDefinition<
|
||||
"media-requests-list",
|
||||
{
|
||||
replaceLinksWithExternalHost: boolean;
|
||||
openInNewTab: boolean;
|
||||
}
|
||||
>;
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrMediaRequestStatsDefinition = CommonOldmarrWidgetDefinition<
|
||||
"media-requests-stats",
|
||||
{
|
||||
replaceLinksWithExternalHost: boolean;
|
||||
openInNewTab: boolean;
|
||||
}
|
||||
>;
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export type OldmarrMediaServerDefinition = CommonOldmarrWidgetDefinition<"media-server", {}>;
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrMediaTranscodingDefinition = CommonOldmarrWidgetDefinition<
|
||||
"media-transcoding",
|
||||
{
|
||||
defaultView: "workers" | "queue" | "statistics";
|
||||
showHealthCheck: boolean;
|
||||
showHealthChecksInQueue: boolean;
|
||||
queuePageSize: number;
|
||||
showAppIcon: boolean;
|
||||
}
|
||||
>;
|
||||
10
packages/old-import/src/widgets/definitions/notebook.ts
Normal file
10
packages/old-import/src/widgets/definitions/notebook.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrNotebookDefinition = CommonOldmarrWidgetDefinition<
|
||||
"notebook",
|
||||
{
|
||||
showToolbar: boolean;
|
||||
allowReadOnlyCheck: boolean;
|
||||
content: string;
|
||||
}
|
||||
>;
|
||||
14
packages/old-import/src/widgets/definitions/rss.ts
Normal file
14
packages/old-import/src/widgets/definitions/rss.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrRssDefinition = CommonOldmarrWidgetDefinition<
|
||||
"rss",
|
||||
{
|
||||
rssFeedUrl: string[];
|
||||
refreshInterval: number;
|
||||
dangerousAllowSanitizedItemContent: boolean;
|
||||
textLinesClamp: number;
|
||||
sortByPublishDateAscending: boolean;
|
||||
sortPostsWithoutPublishDateToTheTop: boolean;
|
||||
maximumAmountOfPosts: number;
|
||||
}
|
||||
>;
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrSmartHomeEntityStateDefinition = CommonOldmarrWidgetDefinition<
|
||||
"smart-home/entity-state",
|
||||
{
|
||||
entityId: string;
|
||||
appendUnit: boolean;
|
||||
genericToggle: boolean;
|
||||
automationId: string;
|
||||
displayName: string;
|
||||
displayFriendlyName: boolean;
|
||||
}
|
||||
>;
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrSmartHomeTriggerAutomationDefinition = CommonOldmarrWidgetDefinition<
|
||||
"smart-home/trigger-automation",
|
||||
{
|
||||
automationId: string;
|
||||
displayName: string;
|
||||
}
|
||||
>;
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrTorrentStatusDefinition = CommonOldmarrWidgetDefinition<
|
||||
"torrents-status",
|
||||
{
|
||||
displayCompletedTorrents: boolean;
|
||||
displayActiveTorrents: boolean;
|
||||
speedLimitOfActiveTorrents: number;
|
||||
displayStaleTorrents: boolean;
|
||||
labelFilterIsWhitelist: boolean;
|
||||
labelFilter: string[];
|
||||
displayRatioWithFilter: boolean;
|
||||
columnOrdering: boolean;
|
||||
rowSorting: boolean;
|
||||
columns: ("up" | "down" | "eta" | "progress")[];
|
||||
nameColumnSize: number;
|
||||
}
|
||||
>;
|
||||
4
packages/old-import/src/widgets/definitions/usenet.ts
Normal file
4
packages/old-import/src/widgets/definitions/usenet.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export type OldmarrUsenetDefinition = CommonOldmarrWidgetDefinition<"usenet", {}>;
|
||||
11
packages/old-import/src/widgets/definitions/video-stream.ts
Normal file
11
packages/old-import/src/widgets/definitions/video-stream.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrVideoStreamDefinition = CommonOldmarrWidgetDefinition<
|
||||
"video-stream",
|
||||
{
|
||||
FeedUrl: string;
|
||||
autoPlay: boolean;
|
||||
muted: boolean;
|
||||
controls: boolean;
|
||||
}
|
||||
>;
|
||||
16
packages/old-import/src/widgets/definitions/weather.ts
Normal file
16
packages/old-import/src/widgets/definitions/weather.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { CommonOldmarrWidgetDefinition } from "./common";
|
||||
|
||||
export type OldmarrWeatherDefinition = CommonOldmarrWidgetDefinition<
|
||||
"weather",
|
||||
{
|
||||
displayInFahrenheit: boolean;
|
||||
displayCityName: boolean;
|
||||
displayWeekly: boolean;
|
||||
forecastDays: number;
|
||||
location: {
|
||||
name: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
};
|
||||
}
|
||||
>;
|
||||
121
packages/old-import/src/widgets/options.ts
Normal file
121
packages/old-import/src/widgets/options.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { objectEntries } from "@homarr/common";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import type { WidgetComponentProps } from "../../../widgets/src/definition";
|
||||
import type { OldmarrWidgetDefinitions, WidgetMapping } 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 WidgetMapping]: WidgetMapping[WidgetKey] extends null
|
||||
? null
|
||||
: {
|
||||
[OptionsKey in keyof WidgetComponentProps<WidgetKey>["options"]]: (
|
||||
oldOptions: Extract<OldmarrWidgetDefinitions, { id: WidgetMapping[WidgetKey] }>["options"],
|
||||
) => WidgetComponentProps<WidgetKey>["options"][OptionsKey] | undefined;
|
||||
};
|
||||
};
|
||||
|
||||
const optionMapping: OptionMapping = {
|
||||
"mediaRequests-requestList": {
|
||||
linksTargetNewTab: (oldOptions) => oldOptions.openInNewTab,
|
||||
},
|
||||
"mediaRequests-requestStats": {},
|
||||
calendar: {
|
||||
filterFutureMonths: () => undefined,
|
||||
filterPastMonths: () => undefined,
|
||||
},
|
||||
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,
|
||||
},
|
||||
weather: {
|
||||
forecastDayCount: (oldOptions) => oldOptions.forecastDays,
|
||||
hasForecast: (oldOptions) => oldOptions.displayWeekly,
|
||||
isFormatFahrenheit: (oldOptions) => oldOptions.displayInFahrenheit,
|
||||
location: (oldOptions) => oldOptions.location,
|
||||
showCity: (oldOptions) => oldOptions.displayCityName,
|
||||
},
|
||||
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,
|
||||
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: {},
|
||||
app: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps the oldmarr options to the newmarr options
|
||||
* @param kind item kind to map
|
||||
* @param oldOptions oldmarr options for this item
|
||||
* @returns newmarr options for this item or null if the item did not exist in oldmarr
|
||||
*/
|
||||
export const mapOptions = <K extends WidgetKind>(
|
||||
kind: K,
|
||||
oldOptions: Extract<OldmarrWidgetDefinitions, { id: WidgetMapping[K] }>["options"],
|
||||
) => {
|
||||
logger.debug(`Mapping old homarr options for widget kind=${kind} options=${JSON.stringify(oldOptions)}`);
|
||||
if (optionMapping[kind] === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mapping = optionMapping[kind];
|
||||
return objectEntries(mapping).reduce(
|
||||
(acc, [key, value]) => {
|
||||
const newValue = value(oldOptions as never);
|
||||
logger.debug(`Mapping old homarr option kind=${kind} key=${key as string} newValue=${newValue as string}`);
|
||||
if (newValue !== undefined) {
|
||||
acc[key as string] = newValue;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, unknown>,
|
||||
) as WidgetComponentProps<K>["options"];
|
||||
};
|
||||
Reference in New Issue
Block a user