feat: refine mobile dashboard experience
This commit is contained in:
1 parent
2e3dca3e87
commit
84db2f9d49
68 files changed
+1425
-1311
No files matched your search
@@ -9,10 +9,8 @@ import { clientApi } from "@homarr/api/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { WidgetQueryLoadingState } from "../common/query-state-indicator";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
|
||||
import { BaseWidgetError } from "../errors/base-component";
|
||||
import classes from "./component.module.scss";
|
||||
|
||||
@@ -29,7 +27,6 @@ const useLiveFeed = (input: RouterInputs["widget"]["rssFeed"]["getFeeds"]) => {
|
||||
hasError: false as const,
|
||||
entries: normalizedData,
|
||||
failedFeedCount: 0,
|
||||
isPending: query.isPending,
|
||||
isStale: Boolean(query.error),
|
||||
};
|
||||
}
|
||||
@@ -38,12 +35,11 @@ const useLiveFeed = (input: RouterInputs["widget"]["rssFeed"]["getFeeds"]) => {
|
||||
hasError: false as const,
|
||||
entries: normalizedData?.entries ?? [],
|
||||
failedFeedCount: normalizedData?.failedFeedCount ?? 0,
|
||||
isPending: query.isPending,
|
||||
isStale: Boolean(query.error),
|
||||
};
|
||||
};
|
||||
|
||||
export default function RssFeed({ options, width, height, displayMode }: WidgetComponentProps<"rssFeed">) {
|
||||
export default function RssFeed({ options, width, height }: WidgetComponentProps<"rssFeed">) {
|
||||
const feed = useLiveFeed({
|
||||
urls: options.feedUrls,
|
||||
maximumAmountPosts: typeof options.maximumAmountPosts === "number" ? options.maximumAmountPosts : 100,
|
||||
@@ -61,7 +57,6 @@ export default function RssFeed({ options, width, height, displayMode }: WidgetC
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (feed.isPending) return <WidgetQueryLoadingState />;
|
||||
|
||||
const warning = feed.isStale
|
||||
? t("warning.stale")
|
||||
@@ -69,35 +64,13 @@ export default function RssFeed({ options, width, height, displayMode }: WidgetC
|
||||
? t("warning.partial", { count: feed.failedFeedCount })
|
||||
: undefined;
|
||||
|
||||
if (feed.entries.length === 0) {
|
||||
return (
|
||||
<Flex direction="column" h="100%" p="xs">
|
||||
{warning && (
|
||||
<Alert role="presentation" color="orange" icon={<IconAlertTriangle aria-hidden size={16} />} p="xs">
|
||||
<output>{warning}</output>
|
||||
</Alert>
|
||||
)}
|
||||
<WidgetEmptyState />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
const languageDir = options.enableRtl ? "RTL" : "LTR";
|
||||
|
||||
const isDense = width < 420 || height < 180;
|
||||
const isTiny = width < 260 || height < 110;
|
||||
const isRoomy = width >= 420 && height >= 240;
|
||||
const isAdvanced = displayMode === "advanced";
|
||||
const columns = width >= 720 && height >= 260 ? 2 : 1;
|
||||
const descriptionLines = isRoomy ? Math.max(options.textLinesClamp, 6) : isDense ? 1 : options.textLinesClamp;
|
||||
const entryDisplay = getRssEntryDisplay({
|
||||
isAdvanced,
|
||||
isDense,
|
||||
isTiny,
|
||||
hideDescription: options.hideDescription,
|
||||
showPosterImage: options.showPosterImage,
|
||||
descriptionLines,
|
||||
});
|
||||
const spacing = isRoomy ? "sm" : "xs";
|
||||
|
||||
return (
|
||||
@@ -124,7 +97,7 @@ export default function RssFeed({ options, width, height, displayMode }: WidgetC
|
||||
title={feedEntry.title}
|
||||
>
|
||||
<Group wrap="nowrap" align="flex-start" gap={isDense ? "xs" : "md"}>
|
||||
{feedEntry.enclosure !== undefined && entryDisplay.showImage && (
|
||||
{feedEntry.enclosure !== undefined && options.showPosterImage && !isTiny && (
|
||||
<Image
|
||||
className={classes.poster}
|
||||
src={feedEntry.enclosure}
|
||||
@@ -137,29 +110,18 @@ export default function RssFeed({ options, width, height, displayMode }: WidgetC
|
||||
)}
|
||||
|
||||
<Flex gap={isRoomy ? "sm" : 6} direction="column" w="100%" miw={0}>
|
||||
<Text
|
||||
dir={languageDir}
|
||||
fz={isRoomy ? "md" : "sm"}
|
||||
fw={600}
|
||||
lh={1.25}
|
||||
lineClamp={isAdvanced ? undefined : 2}
|
||||
>
|
||||
<Text dir={languageDir} fz={isRoomy ? "md" : "sm"} fw={600} lh={1.25} lineClamp={2}>
|
||||
{feedEntry.title}
|
||||
</Text>
|
||||
{entryDisplay.showDescription && feedEntry.description && (
|
||||
<Text dir={languageDir} c="dimmed" size="sm" lineClamp={entryDisplay.descriptionLineClamp}>
|
||||
{!options.hideDescription && feedEntry.description && !isTiny && (
|
||||
<Text dir={languageDir} c="dimmed" size="sm" lineClamp={descriptionLines}>
|
||||
{feedDescriptionToText(feedEntry.description)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<InfoDisplay
|
||||
source={entryDisplay.showSource ? getHostname(feedEntry.feedUrl) : undefined}
|
||||
source={!isDense ? getHostname(feedEntry.feedUrl) : undefined}
|
||||
date={feedEntry.published ? dayjs(feedEntry.published).fromNow() : undefined}
|
||||
timestamp={
|
||||
isAdvanced && feedEntry.published
|
||||
? dayjs(feedEntry.published).format("YYYY-MM-DD HH:mm:ss Z")
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</Group>
|
||||
@@ -189,44 +151,15 @@ export const getHostname = (url: string): string => {
|
||||
}
|
||||
};
|
||||
|
||||
interface RssEntryDisplayInput {
|
||||
isAdvanced: boolean;
|
||||
isDense: boolean;
|
||||
isTiny: boolean;
|
||||
hideDescription: boolean;
|
||||
showPosterImage: boolean;
|
||||
descriptionLines: number;
|
||||
}
|
||||
|
||||
export const getRssEntryDisplay = ({
|
||||
isAdvanced,
|
||||
isDense,
|
||||
isTiny,
|
||||
hideDescription,
|
||||
showPosterImage,
|
||||
descriptionLines,
|
||||
}: RssEntryDisplayInput) => ({
|
||||
showDescription: isAdvanced || (!hideDescription && !isTiny),
|
||||
showImage: isAdvanced || (showPosterImage && !isTiny),
|
||||
showSource: isAdvanced || !isDense,
|
||||
descriptionLineClamp: isAdvanced ? undefined : descriptionLines,
|
||||
});
|
||||
|
||||
const InfoDisplay = ({ date, timestamp, source }: { date?: string; timestamp?: string; source?: string }) => (
|
||||
<Group gap={5} align="center" wrap={timestamp ? "wrap" : "nowrap"}>
|
||||
const InfoDisplay = ({ date, source }: { date?: string; source?: string }) => (
|
||||
<Group gap={5} align="center" wrap="nowrap">
|
||||
{date && <IconClock size="1rem" color="var(--mantine-color-dimmed)" />}
|
||||
{date && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{date}
|
||||
</Text>
|
||||
)}
|
||||
{timestamp && <Text c="dimmed">•</Text>}
|
||||
{timestamp && (
|
||||
<Text size="xs" c="dimmed" ff="monospace">
|
||||
{timestamp}
|
||||
</Text>
|
||||
)}
|
||||
{(date || timestamp) && source && <Text c="dimmed">•</Text>}
|
||||
{date && source && <Text c="dimmed">•</Text>}
|
||||
{source && (
|
||||
<Text size="xs" c="dimmed" truncate="end">
|
||||
{source}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { optionsBuilder } from "../options";
|
||||
* - https://www.jsonfeed.org/version/1.1/
|
||||
*/
|
||||
export const { definition, componentLoader } = createWidgetDefinition("rssFeed", {
|
||||
supportsAdvancedFocus: true,
|
||||
supportsAdvancedFocus: false,
|
||||
icon: IconRss,
|
||||
queryKey: [["widget", "rssFeed", "getFeeds"]],
|
||||
queryMatcher: ({ input }, scope) =>
|
||||
|
||||
Reference in new issue
Block a user