Compare commits

..
Author SHA1 Message Date
Thomas 6370c4c3e7 Merge branch 'dev' into fix-colors 2026-06-03 08:42:07 +02:00
homarr-renovate[bot] 3ff0d72ba1 chore(deps): update dependency next-intl to v4.13.0 (#5853)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-03 04:21:57 +00:00
homarr-renovate[bot] a63a191203 chore(deps): update turbo monorepo to ^2.9.16 (#5851)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-02 23:54:09 +00:00
homarr-renovate[bot] 1be906a0cd chore(deps): update dependency isomorphic-dompurify to ^3.15.0 (#5852)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-02 21:55:53 +00:00
ajnart ffd02721bd style: auto-fix lint and format 2026-06-02 21:34:32 +00:00
Thomas Camlong adef3aff65 fix(ui): restore pre-v9 light variant colors with v8CssVariablesResolver
Mantine v9 changed the light variant to use solid colors instead of
transparent ones, altering the visual appearance of the primary red
across NavLinks, badges, and other light-variant components. Apply
v8CssVariablesResolver to both MantineProvider instances to restore
the original Homarr color rendering.
2026-06-02 23:33:17 +02:00
homarr-renovate[bot] 63d8fa8c26 chore(deps): update dependency @ctrl/qbittorrent to ^9.12.2 (#5834)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-02 18:47:29 +00:00
homarr-renovate[bot] b5dd6ae8ef chore(deps): update babel monorepo to >=7.29.7 (#5830)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-02 14:11:26 +00:00
homarr-renovate[bot] d540ad374a chore(deps): update pnpm to v10.34.0 (#5846)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-02 09:17:24 +00:00
homarr-renovate[bot] 6a971c1986 chore(deps): update mantine monorepo to ^9.2.2 (#5844)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-02 04:13:28 +00:00
homarr-crowdin[bot] d86fe3ddf0 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-06-02 02:23:34 +00:00
homarr-renovate[bot] 06db8960a7 chore(deps): update turbo monorepo to ^2.9.15 (#5841)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-01 23:12:09 +00:00
homarr-renovate[bot] a5719acf34 chore(deps): update dependency tmp@<=0.2.3 to >=0.2.7 (#5848)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-01 21:06:21 +00:00
homarr-renovate[bot] d1d597c8e1 chore(deps): update testcontainers-node monorepo to ^12.0.1 (#5845)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-01 16:48:41 +00:00
homarr-renovate[bot] 657e42e4bb chore(deps): update dependency mysql2 to v3.22.4 (#5839)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-31 18:12:08 +00:00
homarr-renovate[bot] 256ebcb864 chore(deps): update dependency oxlint to ^1.67.0 (#5837)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-31 16:18:04 +00:00
homarr-renovate[bot] 72e86dee01 chore(deps): update dependency ioredis to v5.11.0 (#5838)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-31 14:53:18 +00:00
homarr-renovate[bot] b7910b16e0 chore(deps): update dependency oxfmt to ^0.52.0 (#5836)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-31 11:08:29 +00:00
homarr-renovate[bot] 5a83bb6e39 chore(deps): update dependency dayjs to ^1.11.21 (#5835)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-31 08:54:20 +00:00
homarr-renovate[bot] 433b66582d chore(deps): update dependency hono@<4.6.5 to >=4.12.23 (#5828)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-30 09:25:22 +00:00
homarr-renovate[bot] acbcb6818b chore(deps): update dependency react-error-boundary to ^6.1.2 (#5829)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-30 06:54:05 +00:00
homarr-crowdin[bot] 07d991a0a1 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-05-30 05:27:45 +00:00
homarr-renovate[bot] 92364e9574 chore(deps): update tanstack-query monorepo to ^5.100.14 (#5813)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-29 21:15:51 +00:00
ThomasandMeier Lukas 2991777b0a feat(users): rework general settings (#4915)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-29 21:22:55 +02:00
ThomasandMeier Lukas 6cc2ac56a9 fix: prevent GitHub releases check from blocking app on API failure (#5807)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-29 21:14:51 +02:00
homarr-update-contributors[bot] 92086e060f chore: update contributors (#5818) 2026-05-29 20:52:54 +02:00
homarr-renovate[bot] 2aa5ab95e6 chore(deps): update dependency trpc-to-openapi to ^3.3.0 (#5802)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-29 11:44:26 +00:00
Meier Lukas 650f761325 ci(format): fails to apply format due to conflict between --write and --check (#5815) 2026-05-29 11:07:48 +02:00
David be61602576 feat(auth): support custom token endpoint auth methods (#5806) 2026-05-29 10:51:28 +02:00
homarr-crowdin[bot] d01834d975 chore(lang): updated translations from crowdin (#5814) 2026-05-29 10:07:48 +02:00
Thomas Camlong 4a67bf0ef3 fix(ui): use imported theme instead of undefined createTheme
createTheme was called without being imported, causing a runtime
ReferenceError on every page load. Use the already-imported theme
object from @homarr/ui which contains the full Mantine configuration.
2026-05-28 15:54:37 +02:00
Thomas 4454e3fb7a fix: matine ui fixes for concistency (#5733) 2026-05-28 10:59:03 +02:00
100 changed files with 1506 additions and 1241 deletions

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@
"build": "pnpm with-env next build",
"clean": "git clean -xdf .next .turbo node_modules",
"dev": "pnpm with-env next dev",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"start": "pnpm with-env next start",
"typecheck": "tsc --noEmit",
@@ -2,7 +2,7 @@
import type { PropsWithChildren } from "react";
import type { MantineColorScheme, MantineColorSchemeManager } from "@mantine/core";
import { DirectionProvider, MantineProvider } from "@mantine/core";
import { DirectionProvider, MantineProvider, v8CssVariablesResolver } from "@mantine/core";
import dayjs from "dayjs";
import { clientApi } from "@homarr/api/client";
@@ -21,11 +21,8 @@ export const CustomMantineProvider = ({
<MantineProvider
defaultColorScheme={defaultColorScheme}
colorSchemeManager={manager}
theme={createTheme({
primaryColor: "red",
autoContrast: true,
defaultRadius: "sm",
})}
theme={theme}
cssVariablesResolver={v8CssVariablesResolver}
>
{children}
</MantineProvider>
@@ -2,7 +2,7 @@
import type { PropsWithChildren } from "react";
import type { MantineColorsTuple } from "@mantine/core";
import { colorsTuple, createTheme, darken, lighten, MantineProvider, rem } from "@mantine/core";
import { colorsTuple, createTheme, darken, lighten, MantineProvider, rem, v8CssVariablesResolver } from "@mantine/core";
import { useRequiredBoard } from "@homarr/boards/context";
import type { ColorScheme } from "@homarr/definitions";
@@ -33,7 +33,12 @@ export const BoardMantineProvider = ({
});
return (
<MantineProvider defaultColorScheme={defaultColorScheme} theme={theme} colorSchemeManager={colorSchemeManager}>
<MantineProvider
defaultColorScheme={defaultColorScheme}
theme={theme}
colorSchemeManager={colorSchemeManager}
cssVariablesResolver={v8CssVariablesResolver}
>
{children}
</MantineProvider>
);
@@ -185,7 +185,7 @@ export default async function AboutPage() {
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3, lg: 4, xl: 5 }} spacing="md">
{Object.entries(dependencies)
.filter(([, value]) => !value.includes("workspace:"))
.toSorted(([key1], [key2]) => key1.localeCompare(key2))
.sort(([key1], [key2]) => key1.localeCompare(key2))
.map(([name, version]) => (
<UnstyledButton
key={name}
@@ -48,7 +48,7 @@ export default async function CertificatesPage({ params }: CertificatesPageProps
} as const;
}
})
.toSorted((certA, certB) => {
.sort((certA, certB) => {
if (certA.isError) return -1;
if (certB.isError) return 1;
return certA.x509.validToDate.getTime() - certB.x509.validToDate.getTime();
@@ -1,85 +0,0 @@
"use client";
import { Button, Group, Stack } from "@mantine/core";
import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { userChangeHomeBoardsSchema } from "@homarr/validation/user";
import type { Board } from "~/app/[locale]/boards/_types";
import { BoardSelect } from "~/components/board/board-select";
interface ChangeHomeBoardFormProps {
user: RouterOutputs["user"]["getById"];
boardsData: Pick<Board, "id" | "name" | "logoImageUrl">[];
}
export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.changeHomeBoards.useMutation({
async onSettled() {
await revalidatePathActionAsync(`/manage/users/${user.id}`);
},
onSuccess(_, variables) {
form.setInitialValues({
homeBoardId: variables.homeBoardId,
mobileHomeBoardId: variables.mobileHomeBoardId,
});
showSuccessNotification({
message: t("user.action.changeHomeBoard.notification.success.message"),
});
},
onError() {
showErrorNotification({
message: t("user.action.changeHomeBoard.notification.error.message"),
});
},
});
const form = useZodForm(userChangeHomeBoardsSchema, {
initialValues: {
homeBoardId: user.homeBoardId,
mobileHomeBoardId: user.mobileHomeBoardId,
},
});
const handleSubmit = (values: FormType) => {
mutate({
userId: user.id,
...values,
});
};
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<BoardSelect
label={t("management.page.user.setting.general.item.board.type.general")}
clearable
boards={boardsData}
w="100%"
{...form.getInputProps("homeBoardId")}
/>
<BoardSelect
label={t("management.page.user.setting.general.item.board.type.mobile")}
clearable
boards={boardsData}
w="100%"
{...form.getInputProps("mobileHomeBoardId")}
/>
<Group justify="end">
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
</Stack>
</form>
);
};
type FormType = z.infer<typeof userChangeHomeBoardsSchema>;
@@ -1,84 +0,0 @@
"use client";
import { Button, Group, Select, Stack, Switch } from "@mantine/core";
import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { userChangeSearchPreferencesSchema } from "@homarr/validation/user";
interface ChangeSearchPreferencesFormProps {
user: RouterOutputs["user"]["getById"];
searchEnginesData: { value: string; label: string }[];
}
export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeSearchPreferencesFormProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.changeSearchPreferences.useMutation({
async onSettled() {
await revalidatePathActionAsync(`/manage/users/${user.id}`);
},
onSuccess(_, variables) {
form.setInitialValues({
defaultSearchEngineId: variables.defaultSearchEngineId,
openInNewTab: variables.openInNewTab,
ddgBangsEnabled: variables.ddgBangsEnabled,
});
showSuccessNotification({
message: t("user.action.changeSearchPreferences.notification.success.message"),
});
},
onError() {
showErrorNotification({
message: t("user.action.changeSearchPreferences.notification.error.message"),
});
},
});
const form = useZodForm(userChangeSearchPreferencesSchema, {
initialValues: {
defaultSearchEngineId: user.defaultSearchEngineId,
openInNewTab: user.openSearchInNewTab,
ddgBangsEnabled: user.ddgBangs,
},
});
const handleSubmit = (values: FormType) => {
mutate({
userId: user.id,
...values,
});
};
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<Select
label={t("user.field.defaultSearchEngine.label")}
w="100%"
data={searchEnginesData}
{...form.getInputProps("defaultSearchEngineId")}
/>
<Switch
label={t("user.field.openSearchInNewTab.label")}
{...form.getInputProps("openInNewTab", { type: "checkbox" })}
/>
<Switch
label={t("user.field.ddgBangs.label")}
{...form.getInputProps("ddgBangsEnabled", { type: "checkbox" })}
/>
<Group justify="end">
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
</Stack>
</form>
);
};
type FormType = z.infer<typeof userChangeSearchPreferencesSchema>;
@@ -1,83 +0,0 @@
"use client";
import { Button, Group, Radio, Stack } from "@mantine/core";
import type { DayOfWeek } from "@mantine/dates";
import dayjs from "dayjs";
import localeData from "dayjs/plugin/localeData";
import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { userFirstDayOfWeekSchema } from "@homarr/validation/user";
dayjs.extend(localeData);
interface FirstDayOfWeekProps {
user: RouterOutputs["user"]["getById"];
}
const weekDays = dayjs.weekdays(false);
export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.changeFirstDayOfWeek.useMutation({
async onSettled() {
await revalidatePathActionAsync(`/manage/users/${user.id}`);
},
onSuccess(_, variables) {
form.setInitialValues({
firstDayOfWeek: variables.firstDayOfWeek as DayOfWeek,
});
showSuccessNotification({
message: t("user.action.changeFirstDayOfWeek.notification.success.message"),
});
},
onError() {
showErrorNotification({
message: t("user.action.changeFirstDayOfWeek.notification.error.message"),
});
},
});
const form = useZodForm(userFirstDayOfWeekSchema, {
initialValues: {
firstDayOfWeek: user.firstDayOfWeek as DayOfWeek,
},
});
const handleSubmit = (values: FormType) => {
mutate({
id: user.id,
...values,
});
};
const inputProps = form.getInputProps("firstDayOfWeek");
const onChange = inputProps.onChange as (value: number) => void;
const value = (inputProps.value as number).toString();
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<Radio.Group {...inputProps} value={value} onChange={(nextValue: string) => onChange(parseInt(nextValue, 10))}>
<Group mt="xs">
<Radio value="1" label={weekDays[1]} />
<Radio value="6" label={weekDays[6]} />
<Radio value="0" label={weekDays[0]} />
</Group>
</Radio.Group>
<Group justify="end">
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
</Stack>
</form>
);
};
type FormType = z.infer<typeof userFirstDayOfWeekSchema>;
@@ -0,0 +1,332 @@
"use client";
import { useEffect, useMemo, useRef } from "react";
import {
Button,
Card,
Divider,
Group,
Radio,
Select,
SimpleGrid,
Stack,
Switch,
Text,
TextInput,
Title,
} from "@mantine/core";
import type { DayOfWeek } from "@mantine/dates";
import dayjs from "dayjs";
import localeData from "dayjs/plugin/localeData";
import { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { env } from "@homarr/common/env";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import {
userChangeHomeBoardsSchema,
userChangeSearchPreferencesSchema,
userEditProfileSchema,
userFirstDayOfWeekSchema,
userPingIconsEnabledSchema,
} from "@homarr/validation/user";
import type { Board } from "~/app/[locale]/boards/_types";
import { BoardSelect } from "~/components/board/board-select";
import { CurrentLanguageCombobox } from "~/components/language/current-language-combobox";
dayjs.extend(localeData);
const userGeneralSettingsSchema = z.object({
name: userEditProfileSchema.shape.name,
email: userEditProfileSchema.shape.email,
homeBoardId: userChangeHomeBoardsSchema.shape.homeBoardId,
mobileHomeBoardId: userChangeHomeBoardsSchema.shape.mobileHomeBoardId,
defaultSearchEngineId: userChangeSearchPreferencesSchema.shape.defaultSearchEngineId,
openInNewTab: userChangeSearchPreferencesSchema.shape.openInNewTab,
ddgBangsEnabled: userChangeSearchPreferencesSchema.shape.ddgBangsEnabled,
firstDayOfWeek: userFirstDayOfWeekSchema.shape.firstDayOfWeek,
pingIconsEnabled: userPingIconsEnabledSchema.shape.pingIconsEnabled,
});
type FormValues = z.infer<typeof userGeneralSettingsSchema>;
const FIRST_DAY_OPTIONS: { value: DayOfWeek; labelKey: number }[] = [
{ value: 1, labelKey: 1 },
{ value: 6, labelKey: 6 },
{ value: 0, labelKey: 0 },
];
interface UserGeneralSettingsFormProps {
user: RouterOutputs["user"]["getById"];
boardsData: Pick<Board, "id" | "name" | "logoImageUrl">[];
searchEnginesData: { value: string; label: string }[];
showLanguageSelector?: boolean;
}
const buildInitialValues = (user: RouterOutputs["user"]["getById"]): FormValues => ({
name: user.name ?? "",
email: user.email ?? "",
homeBoardId: user.homeBoardId,
mobileHomeBoardId: user.mobileHomeBoardId,
defaultSearchEngineId: user.defaultSearchEngineId,
openInNewTab: user.openSearchInNewTab,
ddgBangsEnabled: user.ddgBangs,
firstDayOfWeek: user.firstDayOfWeek as DayOfWeek,
pingIconsEnabled: user.pingIconsEnabled,
});
export const UserGeneralSettingsForm = ({
user,
boardsData,
searchEnginesData,
showLanguageSelector = false,
}: UserGeneralSettingsFormProps) => {
const t = useI18n();
const tGeneral = useScopedI18n("management.page.user.setting.general");
const isCredentialsUser = user.provider === "credentials";
const editProfileMutation = clientApi.user.editProfile.useMutation();
const changeHomeBoardsMutation = clientApi.user.changeHomeBoards.useMutation();
const changeSearchPreferencesMutation = clientApi.user.changeSearchPreferences.useMutation();
const changeFirstDayOfWeekMutation = clientApi.user.changeFirstDayOfWeek.useMutation();
const changePingIconsEnabledMutation = clientApi.user.changePingIconsEnabled.useMutation();
const initialValues = buildInitialValues(user);
const initialValuesRef = useRef(initialValues);
const form = useZodForm(userGeneralSettingsSchema, {
initialValues,
});
const isDirtyRef = useRef(false);
isDirtyRef.current = form.isDirty();
const weekDays = useMemo(() => dayjs.weekdays(false), []);
const mutations = [
editProfileMutation,
changeHomeBoardsMutation,
changeSearchPreferencesMutation,
changeFirstDayOfWeekMutation,
changePingIconsEnabledMutation,
];
const isPending = mutations.some((m) => m.isPending);
useEffect(() => {
const handler = (e: BeforeUnloadEvent) => {
if (env.NODE_ENV === "development") return;
if (isDirtyRef.current) e.preventDefault();
};
window.addEventListener("beforeunload", handler);
return () => window.removeEventListener("beforeunload", handler);
}, []);
const handleSubmitAsync = async (values: FormValues) => {
const parsed = userGeneralSettingsSchema.safeParse(values);
if (!parsed.success) return;
const defaults = initialValuesRef.current;
const changed = <K extends keyof FormValues>(...fields: K[]) =>
fields.some((f) => (f === "email" ? (values[f] || null) !== (defaults[f] || null) : values[f] !== defaults[f]));
const saveActions: { when: boolean; action: () => Promise<unknown> }[] = [
{
when: isCredentialsUser && changed("name", "email"),
action: () =>
editProfileMutation.mutateAsync({ id: user.id, name: parsed.data.name, email: parsed.data.email ?? "" }),
},
{
when: changed("homeBoardId", "mobileHomeBoardId"),
action: () =>
changeHomeBoardsMutation.mutateAsync({
userId: user.id,
homeBoardId: parsed.data.homeBoardId,
mobileHomeBoardId: parsed.data.mobileHomeBoardId,
}),
},
{
when: changed("defaultSearchEngineId", "openInNewTab", "ddgBangsEnabled"),
action: () =>
changeSearchPreferencesMutation.mutateAsync({
userId: user.id,
defaultSearchEngineId: parsed.data.defaultSearchEngineId,
openInNewTab: parsed.data.openInNewTab,
ddgBangsEnabled: parsed.data.ddgBangsEnabled,
}),
},
{
when: changed("firstDayOfWeek"),
action: () =>
changeFirstDayOfWeekMutation.mutateAsync({ id: user.id, firstDayOfWeek: parsed.data.firstDayOfWeek }),
},
{
when: changed("pingIconsEnabled"),
action: () =>
changePingIconsEnabledMutation.mutateAsync({ id: user.id, pingIconsEnabled: parsed.data.pingIconsEnabled }),
},
];
const promises = saveActions.filter((s) => s.when).map((s) => s.action());
if (promises.length === 0) return;
try {
await Promise.all(promises);
const newValues = { ...parsed.data, email: parsed.data.email ?? "" };
initialValuesRef.current = newValues;
form.setInitialValues(newValues);
form.resetDirty();
await revalidatePathActionAsync(`/manage/users/${user.id}`);
showSuccessNotification({
title: t("common.notification.update.success"),
message: t("common.notification.update.success"),
});
} catch {
showErrorNotification({
title: t("common.notification.update.error"),
message: t("common.notification.update.error"),
});
}
};
const handleDiscard = () => {
form.setValues(initialValuesRef.current);
form.resetDirty();
};
const firstDayOfWeekInputProps = form.getInputProps("firstDayOfWeek");
const firstDayOfWeekOnChange = firstDayOfWeekInputProps.onChange as (value: number) => void;
const firstDayOfWeekValue = (firstDayOfWeekInputProps.value as number).toString();
return (
<form onSubmit={form.onSubmit(handleSubmitAsync)}>
<Stack gap="lg">
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg" verticalSpacing="lg">
<Card withBorder bg="transparent">
<Stack gap="md">
<Stack gap={2}>
<Title order={3}>{t("user.name")}</Title>
{!isCredentialsUser && (
<Text c="dimmed" size="sm">
{t("management.page.user.fieldsDisabledExternalProvider")}
</Text>
)}
</Stack>
<Divider />
<TextInput
disabled={!isCredentialsUser}
label={t("user.field.username.label")}
withAsterisk
{...form.getInputProps("name")}
/>
<TextInput
disabled={!isCredentialsUser}
label={t("user.field.email.label")}
{...form.getInputProps("email")}
/>
</Stack>
</Card>
<Card withBorder bg="transparent">
<Stack gap="md">
<Stack gap={2}>
<Title order={3}>{tGeneral("item.board.title")}</Title>
</Stack>
<Divider />
<BoardSelect
label={tGeneral("item.board.type.general")}
clearable
boards={boardsData}
w="100%"
withinPortal
{...form.getInputProps("homeBoardId")}
/>
<BoardSelect
label={tGeneral("item.board.type.mobile")}
clearable
boards={boardsData}
w="100%"
withinPortal
{...form.getInputProps("mobileHomeBoardId")}
/>
</Stack>
</Card>
<Card withBorder bg="transparent">
<Stack gap="md">
<Stack gap={2}>
<Title order={3}>{tGeneral("item.search")}</Title>
</Stack>
<Divider />
<Select
label={t("user.field.defaultSearchEngine.label")}
w="100%"
data={searchEnginesData}
comboboxProps={{ withinPortal: true }}
{...form.getInputProps("defaultSearchEngineId")}
/>
<Switch
label={t("user.field.openSearchInNewTab.label")}
{...form.getInputProps("openInNewTab", { type: "checkbox" })}
/>
<Switch
label={t("user.field.ddgBangs.label")}
{...form.getInputProps("ddgBangsEnabled", { type: "checkbox" })}
/>
</Stack>
</Card>
<Card withBorder bg="transparent">
<Stack gap="md">
<Stack gap={2}>
<Title order={3}>{tGeneral("item.language")}</Title>
</Stack>
<Divider />
{showLanguageSelector && <CurrentLanguageCombobox withinPortal />}
<Title order={4}>{tGeneral("item.firstDayOfWeek")}</Title>
<Radio.Group
{...firstDayOfWeekInputProps}
value={firstDayOfWeekValue}
onChange={(nextValue: string) => firstDayOfWeekOnChange(parseInt(nextValue, 10))}
>
<Group mt="xs" wrap="wrap">
{FIRST_DAY_OPTIONS.map(({ value: dayValue, labelKey }) => (
<Radio key={dayValue} value={dayValue.toString()} label={weekDays[labelKey]} />
))}
</Group>
</Radio.Group>
<Divider my="xs" />
<Title order={4}>{tGeneral("item.accessibility")}</Title>
<Switch
label={t("user.field.pingIconsEnabled.label")}
{...form.getInputProps("pingIconsEnabled", { type: "checkbox" })}
/>
</Stack>
</Card>
</SimpleGrid>
<div style={{ position: "sticky", bottom: 20 }}>
{form.isDirty() && (
<Card withBorder>
<Group justify="space-between" wrap="wrap">
<Text fw={500}>{t("common.unsavedChanges")}</Text>
<Group>
<Button disabled={isPending} variant="default" onClick={handleDiscard}>
{t("common.action.discard")}
</Button>
<Button loading={isPending} type="submit" disabled={!form.isValid()}>
{t("common.action.saveChanges")}
</Button>
</Group>
</Group>
</Card>
)}
</div>
</Stack>
</form>
);
};
@@ -1,69 +0,0 @@
"use client";
import { Button, Group, Stack, Switch } from "@mantine/core";
import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { userPingIconsEnabledSchema } from "@homarr/validation/user";
interface PingIconsEnabledProps {
user: RouterOutputs["user"]["getById"];
}
export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.changePingIconsEnabled.useMutation({
async onSettled() {
await revalidatePathActionAsync(`/manage/users/${user.id}`);
},
onSuccess(_, variables) {
form.setInitialValues({
pingIconsEnabled: variables.pingIconsEnabled,
});
showSuccessNotification({
message: t("user.action.changePingIconsEnabled.notification.success.message"),
});
},
onError() {
showErrorNotification({
message: t("user.action.changePingIconsEnabled.notification.error.message"),
});
},
});
const form = useZodForm(userPingIconsEnabledSchema, {
initialValues: {
pingIconsEnabled: user.pingIconsEnabled,
},
});
const handleSubmit = (values: FormType) => {
mutate({
id: user.id,
...values,
});
};
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<Switch
{...form.getInputProps("pingIconsEnabled", { type: "checkbox" })}
label={t("user.field.pingIconsEnabled.label")}
/>
<Group justify="end">
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
</Stack>
</form>
);
};
type FormType = z.infer<typeof userPingIconsEnabledSchema>;
@@ -2,7 +2,7 @@
import { useCallback } from "react";
import { Box, Button, FileButton, Menu, UnstyledButton } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { useDisclosure, useMediaQuery } from "@mantine/hooks";
import { IconPencil, IconPhotoEdit, IconPhotoX } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
@@ -25,6 +25,7 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
},
});
const [opened, { toggle }] = useDisclosure(false);
const isMobile = useMediaQuery("(max-width: 48em)");
const { openConfirmModal } = useConfirmModal();
const t = useI18n();
const tManageAvatar = useScopedI18n("user.action.manageAvatar");
@@ -94,11 +95,11 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
}, [mutate, user.id, openConfirmModal, tManageAvatar]);
return (
<Box pos="relative">
<Menu opened={opened} keepMounted onChange={toggle} position="bottom-start">
<Box pos="relative" display="flex" style={{ justifyContent: isMobile ? "center" : undefined }}>
<Menu opened={opened} keepMounted onChange={toggle} position="bottom-start" withArrow withinPortal>
<Menu.Target>
<UnstyledButton onClick={toggle}>
<UserAvatar user={user} size={200} />
<UserAvatar user={user} size={isMobile ? 120 : 200} />
<Button
component="div"
pos="absolute"
@@ -1,94 +0,0 @@
"use client";
import { useCallback } from "react";
import { Button, Group, Stack, TextInput } from "@mantine/core";
import type { RouterInputs, RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { userEditProfileSchema } from "@homarr/validation/user";
interface UserProfileFormProps {
user: RouterOutputs["user"]["getById"];
}
export const UserProfileForm = ({ user }: UserProfileFormProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.editProfile.useMutation({
async onSettled() {
await revalidatePathActionAsync("/manage/users");
},
onSuccess(_, variables) {
// Reset form initial values to reset dirty state
form.setInitialValues({
name: variables.name,
email: variables.email ?? "",
});
showSuccessNotification({
title: t("common.notification.update.success"),
message: t("user.action.editProfile.notification.success.message"),
});
},
onError(error) {
const message =
error.data?.code === "CONFLICT"
? t("user.error.usernameTaken")
: t("user.action.editProfile.notification.error.message");
showErrorNotification({
title: t("common.notification.update.error"),
message,
});
},
});
const form = useZodForm(userEditProfileSchema.omit({ id: true }), {
initialValues: {
name: user.name ?? "",
email: user.email ?? "",
},
});
// Only credentials users can edit their profile
const isProviderCredentials = user.provider === "credentials";
const handleSubmit = useCallback(
(values: FormType) => {
if (!isProviderCredentials) return;
mutate({
...values,
id: user.id,
});
},
[isProviderCredentials, mutate, user.id],
);
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack>
<TextInput
disabled={!isProviderCredentials}
label={t("user.field.username.label")}
withAsterisk
{...form.getInputProps("name")}
/>
<TextInput
disabled={!isProviderCredentials}
label={t("user.field.email.label")}
{...form.getInputProps("email")}
/>
{isProviderCredentials && (
<Group justify="end">
<Button type="submit" disabled={!form.isDirty()} loading={isPending}>
{t("common.action.saveChanges")}
</Button>
</Group>
)}
</Stack>
</form>
);
};
type FormType = Omit<RouterInputs["user"]["editProfile"], "id">;
@@ -6,18 +6,13 @@ import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { CurrentLanguageCombobox } from "~/components/language/current-language-combobox";
import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone";
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
import { createMetaTitle } from "~/metadata";
import { canAccessUserEditPage } from "../access";
import { ChangeHomeBoardForm } from "./_components/_change-home-board";
import { ChangeSearchPreferencesForm } from "./_components/_change-search-preferences";
import { DeleteUserButton } from "./_components/_delete-user-button";
import { FirstDayOfWeek } from "./_components/_first-day-of-week";
import { PingIconsEnabled } from "./_components/_ping-icons-enabled";
import { UserGeneralSettingsForm } from "./_components/_general-settings-form";
import { UserProfileAvatarForm } from "./_components/_profile-avatar-form";
import { UserProfileForm } from "./_components/_profile-form";
interface Props {
params: Promise<{
@@ -63,7 +58,7 @@ export default async function EditUserPage(props: Props) {
const boards = await api.board.getAllBoards();
const searchEngines = await api.searchEngine.getSelectable();
const isSelf = session?.user.id === user.id;
const isCredentialsUser = user.provider === "credentials";
return (
@@ -73,51 +68,25 @@ export default async function EditUserPage(props: Props) {
{t("management.page.user.fieldsDisabledExternalProvider")}
</Alert>
)}
<Title>{tGeneral("title")}</Title>
<Group gap="xl">
<Box flex={1}>
<UserProfileForm user={user} />
<Group gap="xl" align="flex-start" wrap="wrap">
<Box flex={1} miw={{ base: "100%", md: 540 }}>
<UserGeneralSettingsForm
user={user}
boardsData={boards.map((board) => ({
id: board.id,
name: board.name,
logoImageUrl: board.logoImageUrl,
}))}
searchEnginesData={searchEngines}
showLanguageSelector={isSelf}
/>
</Box>
<Box w={{ base: "100%", lg: 200 }}>
<Box w={{ base: "100%", lg: 260 }}>
<UserProfileAvatarForm user={user} />
</Box>
</Group>
{session?.user.id === user.id && (
<Stack mb="lg">
<Title order={2}>{tGeneral("item.language")}</Title>
<CurrentLanguageCombobox />
</Stack>
)}
<Stack mb="lg">
<Title order={2}>{tGeneral("item.board.title")}</Title>
<ChangeHomeBoardForm
user={user}
boardsData={boards.map((board) => ({
id: board.id,
name: board.name,
logoImageUrl: board.logoImageUrl,
}))}
/>
</Stack>
<Stack mb="lg">
<Title order={2}>{tGeneral("item.search")}</Title>
<ChangeSearchPreferencesForm user={user} searchEnginesData={searchEngines} />
</Stack>
<Stack mb="lg">
<Title order={2}>{tGeneral("item.firstDayOfWeek")}</Title>
<FirstDayOfWeek user={user} />
</Stack>
<Stack mb="lg">
<Title order={2}>{tGeneral("item.accessibility")}</Title>
<PingIconsEnabled user={user} />
</Stack>
<DangerZoneRoot>
<DangerZoneItem
label={t("user.action.delete.label")}
@@ -6,11 +6,20 @@ import { LanguageCombobox } from "./language-combobox";
interface CurrentLanguageComboboxProps {
width?: string;
withinPortal?: boolean;
}
export const CurrentLanguageCombobox = ({ width }: CurrentLanguageComboboxProps) => {
export const CurrentLanguageCombobox = ({ width, withinPortal }: CurrentLanguageComboboxProps) => {
const currentLocale = useCurrentLocale();
const { changeLocale, isPending } = useChangeLocale();
return <LanguageCombobox value={currentLocale} onChange={changeLocale} isPending={isPending} width={width} />;
return (
<LanguageCombobox
value={currentLocale}
onChange={changeLocale}
isPending={isPending}
width={width}
withinPortal={withinPortal}
/>
);
};
@@ -16,9 +16,17 @@ interface LanguageComboboxProps {
onChange: (value: SupportedLanguage) => void;
isPending?: boolean;
width?: string;
withinPortal?: boolean;
}
export const LanguageCombobox = ({ label, value, onChange, isPending, width }: LanguageComboboxProps) => {
export const LanguageCombobox = ({
label,
value,
onChange,
isPending,
width,
withinPortal = false,
}: LanguageComboboxProps) => {
const combobox = useCombobox({
onDropdownClose: () => combobox.resetSelectedOption(),
});
@@ -39,7 +47,7 @@ export const LanguageCombobox = ({ label, value, onChange, isPending, width }: L
}, [combobox]);
return (
<Combobox store={combobox} onOptionSubmit={handleOnOptionSubmit}>
<Combobox store={combobox} withinPortal={withinPortal} onOptionSubmit={handleOnOptionSubmit}>
<Combobox.Target>
<InputBase
component="button"
+1 -1
View File
@@ -12,7 +12,7 @@
"scripts": {
"clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --"
+1 -1
View File
@@ -12,7 +12,7 @@
"scripts": {
"clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --"
+2 -2
View File
@@ -6,9 +6,9 @@
},
"engines": {
"node": ">=24.16.0",
"pnpm": ">=10.33.4"
"pnpm": ">=10.34.1"
},
"packageManager": "pnpm@10.33.4",
"packageManager": "pnpm@10.34.1",
"pnpm": {
"onlyBuiltDependencies": [
"better-sqlite3"
+8 -8
View File
@@ -17,8 +17,8 @@
"db:studio": "pnpm -F db studio",
"dev": "pnpm -F nextjs dev",
"docker:dev": "docker compose -f ./development/development.docker-compose.yml up",
"format": "turbo format --continue",
"format:fix": "turbo format --continue -- --write",
"format": "turbo format --continue -- --check",
"format:fix": "turbo format --continue",
"lint": "turbo lint --continue",
"lint:fix": "turbo lint --continue -- --fix",
"lint:ws": "pnpm dlx sherif@latest",
@@ -60,9 +60,9 @@
},
"engines": {
"node": ">=24.16.0",
"pnpm": ">=10.33.4"
"pnpm": ">=10.34.1"
},
"packageManager": "pnpm@10.33.4",
"packageManager": "pnpm@10.34.1",
"pnpm": {
"onlyBuiltDependencies": [
"@tree-sitter-grammars/tree-sitter-yaml",
@@ -76,14 +76,14 @@
"tree-sitter-json"
],
"overrides": {
"@babel/helpers@<7.26.10": ">=7.29.2",
"@babel/runtime@<7.26.10": ">=7.29.2",
"@babel/helpers@<7.26.10": ">=7.29.7",
"@babel/runtime@<7.26.10": ">=7.29.7",
"axios@>=1.0.0 <1.8.2": ">=1.16.1",
"brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.6",
"brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.6",
"esbuild@<=0.24.2": ">=0.28.0",
"form-data@>=4.0.0 <4.0.4": ">=4.0.5",
"hono@<4.6.5": ">=4.12.22",
"hono@<4.6.5": ">=4.12.23",
"linkifyjs@<4.3.2": ">=4.3.3",
"nanoid@>=4.0.0 <5.0.9": ">=5.1.11",
"prismjs@<1.30.0": ">=1.30.0",
@@ -93,7 +93,7 @@
"sha.js@<=2.4.11": ">=2.4.12",
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.2",
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.2",
"tmp@<=0.2.3": ">=0.2.5",
"tmp@<=0.2.3": ">=0.2.7",
"vite@>=5.0.0 <=5.4.18": ">=8.0.14"
},
"patchedDependencies": {
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+3
View File
@@ -40,6 +40,9 @@ export const env = createEnv({
AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE: z.string().optional(),
AUTH_OIDC_FORCE_USERINFO: createBooleanSchema(false),
AUTH_OIDC_ENABLE_DANGEROUS_ACCOUNT_LINKING: createBooleanSchema(false),
AUTH_OIDC_TOKEN_ENDPOINT_AUTH_METHOD: z
.enum(["client_secret_basic", "client_secret_post", "client_secret_jwt", "none"])
.default("client_secret_basic"),
}
: {}),
...(authProviders.includes("ldap")
+1 -1
View File
@@ -18,7 +18,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
@@ -14,6 +14,9 @@ export const OidcProvider = (headers: ReadonlyHeaders | null): OIDCConfig<Profil
type: "oidc",
clientId: env.AUTH_OIDC_CLIENT_ID,
clientSecret: env.AUTH_OIDC_CLIENT_SECRET,
client: {
token_endpoint_auth_method: env.AUTH_OIDC_TOKEN_ENDPOINT_AUTH_METHOD,
},
issuer: env.AUTH_OIDC_ISSUER,
allowDangerousEmailAccountLinking: env.AUTH_OIDC_ENABLE_DANGEROUS_ACCOUNT_LINKING,
authorization: {
+1 -1
View File
@@ -18,7 +18,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -17,7 +17,7 @@
"scripts": {
"build": "esbuild src/index.ts --bundle --platform=node --outfile=cli.cjs --external:bcrypt --external:cpu-features --loader:.html=text --loader:.node=text",
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"run": "pnpm with-env node cli.cjs",
"typecheck": "tsc --noEmit",
+1 -1
View File
@@ -20,7 +20,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -31,7 +31,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -17,7 +17,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -18,7 +18,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -17,7 +17,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
@@ -1,13 +1,11 @@
import { EVERY_HOUR } from "@homarr/cron-jobs-core/expressions";
import { EVERY_DAY } from "@homarr/cron-jobs-core/expressions";
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
import { createCronJob } from "../lib";
export const updateCheckerJob = createCronJob("updateChecker", EVERY_HOUR, {
export const updateCheckerJob = createCronJob("updateChecker", EVERY_DAY, {
runOnStart: true,
}).withCallback(async () => {
const handler = updateCheckerRequestHandler.handler({});
await handler.getCachedOrUpdatedDataAsync({
forceUpdate: true,
});
await handler.getCachedOrUpdatedDataAsync({});
});
+1 -1
View File
@@ -20,7 +20,7 @@
"build:postgresql": "esbuild migrations/postgresql/migrate.ts --bundle --platform=node --outfile=migrations/postgresql/migrate.cjs",
"build:sqlite": "esbuild migrations/sqlite/migrate.ts --bundle --platform=node --outfile=migrations/sqlite/migrate.cjs",
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"migration:custom": "pnpm with-env tsx ./migrations/custom/run-custom.ts",
"migration:mysql:drop": "pnpm with-env drizzle-kit drop --config ./configs/mysql.config.ts",
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"postinstall": "tsx ./src/docs/codegen.ts",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
+1 -1
View File
@@ -18,7 +18,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -17,7 +17,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -17,7 +17,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -19,7 +19,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -17,7 +17,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -20,7 +20,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
@@ -1,6 +1,7 @@
import dayjs from "dayjs";
import type { Duration } from "dayjs/plugin/duration";
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { createChannelWithLatestAndEvents } from "@homarr/redis";
@@ -15,6 +16,7 @@ interface Options<TData, TInput extends Record<string, unknown>> {
options: Options<TData, TInput>,
) => ReturnType<typeof createChannelWithLatestAndEvents<TData>>;
cacheDuration: Duration;
fallbackToStaleOnError?: boolean;
}
export const createCachedRequestHandler = <TData, TInput extends Record<string, unknown>>(
@@ -26,13 +28,33 @@ export const createCachedRequestHandler = <TData, TInput extends Record<string,
return {
async getCachedOrUpdatedDataAsync({ forceUpdate = false }) {
const channelData = await channel.getAsync();
const requestNewDataAsync = async () => {
const data = await options.requestAsync(input);
await channel.publishAndUpdateLastStateAsync(data);
return {
data,
timestamp: new Date(),
};
try {
const data = await options.requestAsync(input);
await channel.publishAndUpdateLastStateAsync(data);
return {
data,
timestamp: new Date(),
};
} catch (error) {
if (options.fallbackToStaleOnError && channelData) {
logger.warn(
new ErrorWithMetadata(
"Cached request handler using stale cache after fetch failure",
{
channel: channel.name,
queryKey: options.queryKey,
},
{ cause: error },
),
);
return channelData;
}
throw error;
}
};
if (forceUpdate) {
@@ -43,8 +65,6 @@ export const createCachedRequestHandler = <TData, TInput extends Record<string,
return await requestNewDataAsync();
}
const channelData = await channel.getAsync();
const shouldRequestNewData =
!channelData ||
dayjs().diff(channelData.timestamp, "milliseconds") > options.cacheDuration.asMilliseconds();
@@ -111,6 +111,24 @@ describe("getAvailableUpdatesAsync", () => {
// Assert
expect(result).toEqual([]);
});
test("should propagate GitHub API auth errors without blocking callers", async () => {
const currentVersion = "v1.2.3";
const listReleasesSpy = vi.spyOn(new Octokit().rest.repos, "listReleases");
listReleasesSpy.mockRejectedValue(new Error("Bad credentials"));
await expect(getAvailableUpdatesAsync(currentVersion)).rejects.toThrow("Bad credentials");
});
test("should propagate GitHub API quota exhausted errors without blocking callers", async () => {
const currentVersion = "v1.2.3";
const listReleasesSpy = vi.spyOn(new Octokit().rest.repos, "listReleases");
listReleasesSpy.mockRejectedValue(
new Error("Request quota exhausted for request GET /repos/{owner}/{repo}/releases"),
);
await expect(getAvailableUpdatesAsync(currentVersion)).rejects.toThrow("quota exhausted");
});
});
const fakeReleases = (...inputs: [string, boolean][]) => inputs.map(fakeRelease);
@@ -15,7 +15,8 @@ const logger = createLogger({ module: "updateCheckerRequestHandler" });
export const updateCheckerRequestHandler = createCachedRequestHandler({
queryKey: "homarr-update-checker",
cacheDuration: dayjs.duration(1, "hour"),
cacheDuration: dayjs.duration(1, "day"),
fallbackToStaleOnError: true,
async requestAsync(_) {
return {
availableUpdates: await getAvailableUpdatesAsync(packageJson.version),
@@ -58,6 +59,7 @@ export const getAvailableUpdatesAsync = async (currentVersion: string) => {
request: {
fetch: fetchWithTrustedCertificatesAsync,
},
throttle: { enabled: false },
});
const isCurrentPrerelease = isPrereleaseTag(currentVersion);
@@ -100,7 +102,7 @@ export const getAvailableUpdatesAsync = async (currentVersion: string) => {
const availableUpdates = semanticReleases
.filter((release) => isCurrentPrerelease || !release.isPrerelease)
.filter((release) => compareSemVer(release.tagName, currentVersion) > 0)
.sort((releaseA, releaseB) => compareSemVer(releaseB.tagName, releaseA.tagName));
.toSorted((releaseA, releaseB) => compareSemVer(releaseB.tagName, releaseA.tagName));
if (availableUpdates.length === 0) {
logger.debug("No available updates found", { currentVersion });
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -17,7 +17,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -17,7 +17,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -21,7 +21,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": ""
"changePosition": "",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "打开媒体"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "查阅文档",
"checkLogs": "检查日志获取更多详情",
"tryAgain": "重试",
"loading": "正在加载"
"loading": "正在加载",
"show": "",
"hide": ""
},
"here": "这里",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "新建分类",
"changePosition": "换位"
"changePosition": "换位",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "此面板是公开的",
"private": "此面板是私有的"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "crwdns2680:0crwdne2680:0"
}
},
"noResults": {
"title": "crwdns4251:0crwdne4251:0"
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "crwdns546:0crwdne546:0",
"checkLogs": "crwdns2260:0crwdne2260:0",
"tryAgain": "crwdns548:0crwdne548:0",
"loading": "crwdns550:0crwdne550:0"
"loading": "crwdns550:0crwdne550:0",
"show": "crwdns4255:0crwdne4255:0",
"hide": "crwdns4257:0crwdne4257:0"
},
"here": "crwdns552:0crwdne552:0",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "crwdns686:0crwdne686:0",
"changePosition": "crwdns688:0crwdne688:0"
"changePosition": "crwdns688:0crwdne688:0",
"settings": "crwdns4259:0crwdne4259:0"
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "crwdns1656:0crwdne1656:0",
"private": "crwdns1658:0crwdne1658:0"
},
"noResults": {
"title": "crwdns4253:0crwdne4253:0"
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Přečtěte si dokumentaci",
"checkLogs": "Pro více podrobností zkontrolujte logy",
"tryAgain": "Zkusit znovu",
"loading": "Načítání"
"loading": "Načítání",
"show": "",
"hide": ""
},
"here": "zde",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Nová kategorie",
"changePosition": "Změnit pozici"
"changePosition": "Změnit pozici",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Åben medie"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Tjek dokumentationen",
"checkLogs": "Tjek logs for flere detaljer",
"tryAgain": "Prøv igen",
"loading": "Indlæser"
"loading": "Indlæser",
"show": "",
"hide": ""
},
"here": "her",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Ny kategori",
"changePosition": "Ændre placering"
"changePosition": "Ændre placering",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Denne tavle er offentlig",
"private": "Denne tavle er privat"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Medium öffnen"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Die Dokumentation ansehen",
"checkLogs": "Logs für weitere Details prüfen",
"tryAgain": "Erneut versuchen",
"loading": "Wird geladen"
"loading": "Wird geladen",
"show": "",
"hide": ""
},
"here": "Hier",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Neue Kategorie",
"changePosition": "Position wechseln"
"changePosition": "Position wechseln",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Dieses Board ist öffentlich",
"private": "Dieses Board ist privat"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Medium öffnen"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Die Dokumentation ansehen",
"checkLogs": "Logs für weitere Details prüfen",
"tryAgain": "Erneut versuchen",
"loading": "Wird geladen"
"loading": "Wird geladen",
"show": "",
"hide": ""
},
"here": "Hier",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Neue Kategorie",
"changePosition": "Position wechseln"
"changePosition": "Position wechseln",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Dieses Board ist öffentlich",
"private": "Dieses Board ist privat"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "Προσπαθήστε ξανά",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": "Αλλαγή θέσης"
"changePosition": "Αλλαγή θέσης",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Open media"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Check the documentation",
"checkLogs": "Check logs for more information",
"tryAgain": "Try again",
"loading": "Loading"
"loading": "Loading",
"show": "",
"hide": ""
},
"here": "here",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "New category",
"changePosition": "Change position"
"changePosition": "Change position",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Abrir imagen"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Consultar la documentación",
"checkLogs": "Comprueba los registros para más detalles",
"tryAgain": "Intentar de nuevo",
"loading": "Cargando"
"loading": "Cargando",
"show": "",
"hide": ""
},
"here": "aquí",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Nueva Categoría",
"changePosition": "Cambiar posición"
"changePosition": "Cambiar posición",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Este tablero es público",
"private": "Este tablero es privado"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": ""
"changePosition": "",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Ouvrir le média"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Voir la documentation",
"checkLogs": "Vérifiez les logs pour plus de détails",
"tryAgain": "Réessayer",
"loading": "Chargement"
"loading": "Chargement",
"show": "",
"hide": ""
},
"here": "ici",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Nouvelle catégorie",
"changePosition": "Modifier la position"
"changePosition": "Modifier la position",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Ce tableau est public",
"private": "Ce tableau est privé"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "פתיחת מדיה"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "בדוק את התיעוד",
"checkLogs": "בדוק לוגים לפרטים נוספים",
"tryAgain": "נא לנסות שוב",
"loading": "טוען"
"loading": "טוען",
"show": "",
"hide": ""
},
"here": "כאן",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "קטגוריה חדשה",
"changePosition": "שנה מיקום"
"changePosition": "שנה מיקום",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "הלוח הזה ציבורי",
"private": "הלוח הזה פרטי"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "Pokušaj ponovno",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": "Promijenjen položaj"
"changePosition": "Promijenjen položaj",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Média megnyitása"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Tekintse meg a dokumentációt",
"checkLogs": "További részletekért ellenőrizze a naplókat",
"tryAgain": "Próbálja újra",
"loading": "Töltés"
"loading": "Töltés",
"show": "",
"hide": ""
},
"here": "itt",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Új kategória",
"changePosition": "Pozíció módosítása"
"changePosition": "Pozíció módosítása",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Apri media"
}
},
"noResults": {
"title": "Nessun media caricato"
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Consulta la documentazione",
"checkLogs": "Controlla i log per maggiori dettagli",
"tryAgain": "Riprova",
"loading": "Caricamento"
"loading": "Caricamento",
"show": "Visualizza",
"hide": "Nascondi"
},
"here": "qui",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Nuova categoria",
"changePosition": "Cambia posizione"
"changePosition": "Cambia posizione",
"settings": "Impostazioni"
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Questa board è pubblica",
"private": "Questa board è privata"
},
"noResults": {
"title": "Ancora nessuna board"
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "メディアを開く"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "ドキュメントを確認する",
"checkLogs": "詳細はログを確認してください",
"tryAgain": "リトライ",
"loading": "読み込み中"
"loading": "読み込み中",
"show": "",
"hide": ""
},
"here": "こちら",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "新しいカテゴリー",
"changePosition": "ポジションを変更する"
"changePosition": "ポジションを変更する",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "このボードは公開中です",
"private": "このボードは非公開です"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "다시 시도",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": "위치 변경"
"changePosition": "위치 변경",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": ""
"changePosition": "",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "Mēģiniet vēlreiz",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": "Mainīt pozīciju"
"changePosition": "Mainīt pozīciju",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Media openen"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "De documentatie bekijken",
"checkLogs": "Controleer logs voor meer details",
"tryAgain": "Probeer opnieuw",
"loading": "Laden"
"loading": "Laden",
"show": "",
"hide": ""
},
"here": "hier",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Nieuwe categorie",
"changePosition": "Positie wijzigen"
"changePosition": "Positie wijzigen",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Dit bord is openbaar",
"private": "Dit bord is privé"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Åpne medie"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Les dokumentasjonen",
"checkLogs": "Sjekk loggene for mer informasjon",
"tryAgain": "Prøv igjen",
"loading": "Laster"
"loading": "Laster",
"show": "",
"hide": ""
},
"here": "her",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Ny kategori",
"changePosition": "Endre posisjon"
"changePosition": "Endre posisjon",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Denne tavlen er offentlig",
"private": "Denne tavlen er privat"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Otwórz media"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Sprawdź dokumentację",
"checkLogs": "Sprawdź logi, aby uzyskać więcej informacji.",
"tryAgain": "Spróbuj ponownie",
"loading": "Ładowanie"
"loading": "Ładowanie",
"show": "",
"hide": ""
},
"here": "tutaj",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Nowa kategoria",
"changePosition": "Zmiana pozycji"
"changePosition": "Zmiana pozycji",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Ta tablica jest publiczna",
"private": "Ta tablica jest prywatna"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Abrir mídia"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Confira a documentação",
"checkLogs": "Verifique os logs para mais detalhes",
"tryAgain": "Tente novamente",
"loading": "Carregando"
"loading": "Carregando",
"show": "",
"hide": ""
},
"here": "aqui",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Nova categoria",
"changePosition": "Mudar de posição"
"changePosition": "Mudar de posição",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Este quadro é publico",
"private": "Este quadro é privado"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": ""
"changePosition": "",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "Încearcă din nou",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": "Schimbă locația"
"changePosition": "Schimbă locația",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Открыть медиа"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Посмотреть документацию",
"checkLogs": "Проверить логи для подробностей",
"tryAgain": "Попробовать снова",
"loading": "Загрузка"
"loading": "Загрузка",
"show": "",
"hide": ""
},
"here": "здесь",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Новая категория",
"changePosition": "Изменить позицию"
"changePosition": "Изменить позицию",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Эта панель публичная",
"private": "Эта панель приватная"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Otvoriť médiá"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Pozrite si dokumentáciu",
"checkLogs": "Ďalšie podrobnosti nájdete v protokoloch",
"tryAgain": "Skúste znova",
"loading": "Nahrávam"
"loading": "Nahrávam",
"show": "",
"hide": ""
},
"here": "tu",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Nová kategória",
"changePosition": "Zmeniť pozíciu"
"changePosition": "Zmeniť pozíciu",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Táto tabuľa je verejná",
"private": "Táto tabuľa je súkromná"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "Poskusite znova",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": "Spremeni položaj"
"changePosition": "Spremeni položaj",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+117 -108
View File
@@ -82,7 +82,7 @@
"form": {
"name": {
"label": "Gruppens namn",
"description": ""
"description": "Namnet måste matcha admin-grupp i extern tjänst"
}
}
},
@@ -159,7 +159,7 @@
"label": "Visa sökresultat i ny flik"
},
"ddgBangs": {
"label": ""
"label": "Aktivera DuckDuckGo bangs"
}
},
"error": {
@@ -252,10 +252,10 @@
"changeDdgBangs": {
"notification": {
"success": {
"message": ""
"message": "DuckDuckGo bangs inställningen ändrad"
},
"error": {
"message": ""
"message": "DuckDuckGo bangs inställningen kunde inte ändras"
}
}
},
@@ -350,12 +350,12 @@
"description": "Tillåt medlemmar att addera valfri applikation på sina tavlor"
},
"modify-all": {
"label": "",
"description": ""
"label": "Modifiera alla appar",
"description": "Tillåt att medlemmar modifierar alla appar"
},
"full-all": {
"label": "",
"description": ""
"label": "Fullständiga rättigheter till appar",
"description": "Tillåt att medlemmar hanterar, använder och raderar alla appar"
}
}
},
@@ -363,20 +363,20 @@
"title": "Tavlor",
"item": {
"create": {
"label": "",
"description": ""
"label": "Skapa tavlor",
"description": "Tillåt att medlemmar skapar tavlor"
},
"view-all": {
"label": "",
"description": ""
"label": "Se alla tavlor",
"description": "Tillåt att medlemmar ser alla tavlor"
},
"modify-all": {
"label": "",
"description": ""
"label": "Modifiera alla tavlor",
"description": "Tillåt att medlemmar modifierar alla tavlor (Detta inkluderar inte åtkomstkontroll eller farozonen)"
},
"full-all": {
"label": "",
"description": ""
"label": "Fullständiga rättigheter till tavlor",
"description": "Tillåt att medlemmar ser, modifierar och tar bort alla tavlor (detta inkluderar åtkomstkontroll och farozonen)"
}
}
},
@@ -392,38 +392,38 @@
"description": "Tillåter medlemmar att addera valfri integration till sina tavlor"
},
"interact-all": {
"label": "",
"description": ""
"label": "Interagera med alla integrationer",
"description": "Tillåt att medlemmar interagerar med alla integrationer"
},
"full-all": {
"label": "",
"description": ""
"label": "Fullständiga rättigheter till integrationer",
"description": "Tillåt att medlemmar hanterar, använder och interagerar med alla integrationer"
}
}
},
"media": {
"title": "",
"title": "Media",
"item": {
"upload": {
"label": "",
"description": ""
"label": "Ladda upp media",
"description": "Tillåt att medlemmar laddar upp media"
},
"view-all": {
"label": "",
"description": ""
"label": "Se all media",
"description": "Tillåt att medlemmar ser all media"
},
"full-all": {
"label": "",
"description": ""
"label": "Fullständiga rättigheter till media",
"description": "Tillåt att medlemmar hanterar och raderar all media"
}
}
},
"other": {
"title": "",
"title": "Annat",
"item": {
"view-logs": {
"label": "",
"description": ""
"label": "Se loggar",
"description": "Tillåt att medlemmar ser loggar"
}
}
},
@@ -435,32 +435,32 @@
"description": "Tillåt medlemmar att skapa sökmotorer"
},
"modify-all": {
"label": "",
"description": ""
"label": "Modifiera alla sökmotorer",
"description": "Tillåt att medlemmar modifierar alla sökmotorer"
},
"full-all": {
"label": "",
"description": ""
"label": "Fullständiga rättigheter till sökmotorer",
"description": "Tillåt att medlemmar hanterar och raderar alla sökmotorer"
}
}
}
},
"memberNotice": {
"mixed": "",
"external": ""
"mixed": "Vissa medlemmar är från externa tjänster och kan inte hantera här",
"external": "Alla medlemmar är från externa tjänster och kan inte hanteras här"
},
"reservedNotice": {
"message": ""
"message": "Denna grupp är reserverad för systemet så vissa åtgärder är begränsade. <checkoutDocs></checkoutDocs>"
},
"action": {
"create": {
"label": "Addera en grupp",
"notification": {
"success": {
"message": ""
"message": "Gruppen har skapats"
},
"error": {
"message": ""
"message": "Gruppen kunde inte skapas"
}
}
},
@@ -470,7 +470,7 @@
"confirm": "Är du säker på att du vill överföra ägarskapet för gruppen {name} till {username}?",
"notification": {
"success": {
"message": ""
"message": "Överfört grupp {group} till {user}"
},
"error": {
"message": "Kunde inte överföra ägarskap"
@@ -501,38 +501,38 @@
"notification": {
"success": {
"title": "Behörigheter sparade",
"message": ""
"message": "Rättigheterna sparade"
},
"error": {
"title": "",
"message": ""
"title": "Rättigheterna ej sparade",
"message": "Rättigheterna har inte sparats"
}
}
},
"update": {
"notification": {
"success": {
"message": ""
"message": "Gruppen {name} har sparats"
},
"error": {
"message": ""
"message": "Kunde inte spara gruppen {name}"
}
}
},
"select": {
"label": "Välj en grupp",
"notFound": ""
"notFound": "Ingen grupp hittad"
},
"settings": {
"board": {
"notification": {
"success": {
"title": "Inställningarna sparades",
"message": ""
"message": "Inställningarna för tavlan har sparats"
},
"error": {
"title": "Kunde inte spara inställningarna",
"message": ""
"message": "Kunde inte spara inställningarna för tavlan"
}
}
}
@@ -540,10 +540,10 @@
"changePosition": {
"notification": {
"success": {
"message": ""
"message": "Ordning ändrad"
},
"error": {
"message": ""
"message": "Ordningen kunde ändras"
}
}
}
@@ -559,20 +559,20 @@
"list": {
"title": "Applikationer",
"noResults": {
"title": "",
"action": ""
"title": "Det finns inga appar ännu",
"action": "Skapa din första app"
}
},
"create": {
"title": "Addera applikation",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Skapandet lyckades",
"message": "Appen skapades framgångsrikt"
},
"error": {
"title": "",
"message": ""
"title": "Skapandet misslyckades",
"message": "Appen kunde inte skapas"
}
}
},
@@ -581,11 +581,11 @@
"notification": {
"success": {
"title": "Ändringarna har sparats",
"message": ""
"message": "Appen sparades"
},
"error": {
"title": "Misslyckades med att spara ändringarna",
"message": ""
"message": "Appen kunde inte sparas"
}
}
},
@@ -594,12 +594,12 @@
"message": "Är du säker på att du vill ta bort applikationen {name}?",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Borttagning lyckades",
"message": "Appen har tagits bort"
},
"error": {
"title": "",
"message": ""
"title": "Borttagning misslyckades",
"message": "Kunde inte ta bort appen"
}
}
}
@@ -612,21 +612,21 @@
"label": "Beskrivning"
},
"url": {
"label": ""
"label": "URL"
},
"useDifferentUrlForPing": {
"checkbox": {
"label": "",
"description": ""
"label": "Använd annan URL för ping",
"description": "Användbart om Homarr kan komma åt appen direkt via ett internt host-namn eller nätverk för att undvika bandbreddsanvändning hos internetleverantör"
}
}
},
"action": {
"select": {
"label": "",
"notFound": "",
"label": "Välj app",
"notFound": "Ingen app hittades",
"search": "Sök efter en applikation",
"noResults": "",
"noResults": "Inga resultat",
"action": "Välj {app}",
"title": "Välj en applikation att lägga till på tavlan"
},
@@ -642,56 +642,56 @@
"page": {
"list": {
"title": "Integrationer",
"search": "",
"search": "Sök integrationer",
"noResults": {
"title": ""
"title": "Det finns inga integrationer ännu"
}
},
"create": {
"title": "",
"title": "Ny {name} integration",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Skapandet lyckades",
"message": "Integrationen har skapats"
},
"error": {
"title": "",
"message": ""
"title": "Skapandet misslyckades",
"message": "Integrationen kunde inte skapas"
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
"title": "Befintlig",
"label": "Välj befintlig app"
},
"new": {
"title": "",
"title": "Ny",
"url": {
"label": "",
"description": ""
"label": "App URL",
"description": "URL:en som appen kommer att öppna när den används från tavlan"
}
}
}
}
},
"edit": {
"title": "",
"title": "Redigera {name} integration",
"notification": {
"success": {
"title": "Ändringarna har sparats",
"message": ""
"message": "Integrationen sparad"
},
"error": {
"title": "",
"message": ""
"title": "Kunde inte tillämpa ändringar",
"message": "Integrationen kunde inte sparas"
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
"add": "Länka en app",
"remove": "Ta bort länk",
"select": "Välj en app att länka till"
}
}
},
@@ -700,12 +700,12 @@
"message": "Är du säker på att du vill ta bort integrationen {name}?",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Borttagning lyckades",
"message": "Integrationen har tagits bort"
},
"error": {
"title": "",
"message": ""
"title": "Borttagning misslyckades",
"message": "Kunde inte ta bort integrationen"
}
}
}
@@ -715,44 +715,44 @@
"label": "Namn"
},
"url": {
"label": ""
"label": "URL"
},
"attemptSearchEngineCreation": {
"label": "",
"description": ""
"label": "Skapa sökmotor",
"description": "Integrationen \"{kind}\" kan inte användas med sökmotorer. Markera detta för att automatiskt konfigurera sökmotorn."
},
"app": {
"sectionTitle": ""
"sectionTitle": "Länkad app"
},
"createApp": {
"label": "",
"description": ""
"label": "Skapa app",
"description": "Skapa en app med samma namn och ikon som integrationen. Lämna fältet nedan tomt för att skapa en app med integrationens URL."
},
"appHref": {
"placeholder": ""
"placeholder": "Egen app-URL"
}
},
"action": {
"create": ""
"create": "Ny integration"
},
"testConnection": {
"action": {
"create": "",
"edit": ""
"create": "Testa anslutningen och skapa",
"edit": "Testa anslutningen och skapa"
},
"error": {
"common": {
"cause": {
"title": ""
"title": "Orsak med mer detaljer"
}
},
"unknown": {
"title": "",
"description": ""
"title": "Okänt fel",
"description": "Ett okänt fel inträffade, öppna orsaken nedan för att se mer detaljer"
},
"parse": {
"title": "",
"description": ""
"title": "Parsningsfel",
"description": "Svaret kunde inte analyseras. Verifiera att URL pekar på bas-URL för tjänsten."
},
"authorization": {
"title": "",
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "Försök igen",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": "Ändra position"
"changePosition": "Ändra position",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Den här tavlan är offentlig",
"private": "Den här tavlan är privat"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Medyayı aç"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Dökümanlara göz at",
"checkLogs": "Daha fazla ayrıntı için günlükleri kontrol edin",
"tryAgain": "Tekrar Deneyin",
"loading": "Yükleniyor"
"loading": "Yükleniyor",
"show": "",
"hide": ""
},
"here": "buraya",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Yeni Kategori",
"changePosition": "Pozisyonu değiştir"
"changePosition": "Pozisyonu değiştir",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Bu panel herkese açıktır",
"private": "Bu panel özeldir"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "Відкрити медіа"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "Переглянути документацію",
"checkLogs": "Щоб дізнатися більше, перевірте логи",
"tryAgain": "Повторіть спробу",
"loading": "Завантаження"
"loading": "Завантаження",
"show": "",
"hide": ""
},
"here": "тут",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "Нова категорія",
"changePosition": "Змінити положення"
"changePosition": "Змінити положення",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "Ця дошка є публічною",
"private": "Ця дошка є приватною"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": ""
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "",
"checkLogs": "",
"tryAgain": "Thử lại",
"loading": ""
"loading": "",
"show": "",
"hide": ""
},
"here": "",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "",
"changePosition": "Đổi vị trí"
"changePosition": "Đổi vị trí",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "",
"private": ""
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+11 -2
View File
@@ -1054,6 +1054,9 @@
"open": {
"label": "開啟多媒體"
}
},
"noResults": {
"title": ""
}
},
"common": {
@@ -1084,7 +1087,9 @@
"checkoutDocs": "查閱說明書",
"checkLogs": "檢查 Logs 以取得更多詳情",
"tryAgain": "重試",
"loading": "正在讀取"
"loading": "正在讀取",
"show": "",
"hide": ""
},
"here": "此處",
"iconPicker": {
@@ -1249,7 +1254,8 @@
"menu": {
"label": {
"create": "創建分類",
"changePosition": "變更位置"
"changePosition": "變更位置",
"settings": ""
}
},
"openAllInNewTabs": {
@@ -3452,6 +3458,9 @@
"public": "此面板是公開的",
"private": "此面板是私人的"
},
"noResults": {
"title": ""
},
"modal": {
"createBoard": {
"field": {
+1 -1
View File
@@ -19,7 +19,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
@@ -20,6 +20,7 @@ export interface SelectWithCustomItemsProps<TSelectItem extends BaseSelectItem>
onBlur?: (event: React.FocusEvent<HTMLButtonElement>) => void;
onFocus?: (event: React.FocusEvent<HTMLButtonElement>) => void;
w?: string;
withinPortal?: boolean;
}
type Props<TSelectItem extends BaseSelectItem> = SelectWithCustomItemsProps<TSelectItem> & {
@@ -35,6 +36,7 @@ export const SelectWithCustomItems = <TSelectItem extends BaseSelectItem>({
SelectOption,
w,
clearable,
withinPortal = false,
...props
}: Props<TSelectItem>) => {
const combobox = useCombobox({
@@ -71,7 +73,7 @@ export const SelectWithCustomItems = <TSelectItem extends BaseSelectItem>({
const _clearable = clearable && Boolean(_value);
return (
<Combobox store={combobox} withinPortal={false} onOptionSubmit={onOptionSubmit}>
<Combobox store={combobox} withinPortal={withinPortal} onOptionSubmit={onOptionSubmit}>
<Combobox.Target>
<InputBase
{...props}
+1 -1
View File
@@ -16,7 +16,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -19,7 +19,7 @@
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "oxfmt --check .",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
@@ -39,7 +39,7 @@ export const ResourceTable = ({ type, data, isTiny }: ResourceTableProps) => {
</TableThead>
<TableTbody>
{data
.toSorted((itemA, itemB) => {
.sort((itemA, itemB) => {
const nodeResult = itemA.node.localeCompare(itemB.node);
if (nodeResult !== 0) return nodeResult;
return itemA.name.localeCompare(itemB.name);
@@ -43,7 +43,7 @@ export default function MediaServerWidget({
const newData = filteredData.concat(
data.requests.map((request) => ({ ...request, integrationId: data.integrationId })),
);
return newData.toSorted((dataA, dataB) => {
return newData.sort((dataA, dataB) => {
if (dataA.status === dataB.status) {
return dataB.createdAt.getTime() - dataA.createdAt.getTime();
}
+1 -1
View File
@@ -26,7 +26,7 @@ const useLiveFeedEntries = (input: RouterInputs["widget"]["rssFeed"]["getFeeds"]
return oldData
?.filter((entry) => entry.feedUrl !== updatedData.url)
.concat(updatedData.entries)
.toSorted((entryA, entryB) => {
.sort((entryA, entryB) => {
return entryA.published && entryB.published
? new Date(entryB.published).getTime() - new Date(entryA.published).getTime()
: 0;
+498 -491
View File
File diff suppressed because it is too large. Load diff
+30 -30
View File
@@ -6,7 +6,7 @@ catalog:
"@auth/core": ^0.41.2
"@auth/drizzle-adapter": ^1.11.2
"@ctrl/deluge": ^8.0.0
"@ctrl/qbittorrent": ^9.12.1
"@ctrl/qbittorrent": ^9.12.2
"@ctrl/transmission": ^7.4.0
"@dnd-kit/core": ^6.3.1
"@dnd-kit/modifiers": ^9.0.0
@@ -20,17 +20,17 @@ catalog:
"@immich/sdk": ^2.7.5
"@jellyfin/sdk": ^0.13.0
"@kubernetes/client-node": ^1.4.0
"@mantine/charts": ^9.2.1
"@mantine/colors-generator": ^9.2.1
"@mantine/core": ^9.2.1
"@mantine/dates": ^9.2.1
"@mantine/dropzone": ^9.2.1
"@mantine/form": ^9.2.1
"@mantine/hooks": ^9.2.1
"@mantine/modals": ^9.2.1
"@mantine/notifications": ^9.2.1
"@mantine/spotlight": ^9.2.1
"@mantine/tiptap": ^9.2.1
"@mantine/charts": ^9.2.2
"@mantine/colors-generator": ^9.2.2
"@mantine/core": ^9.2.2
"@mantine/dates": ^9.2.2
"@mantine/dropzone": ^9.2.2
"@mantine/form": ^9.2.2
"@mantine/hooks": ^9.2.2
"@mantine/modals": ^9.2.2
"@mantine/notifications": ^9.2.2
"@mantine/spotlight": ^9.2.2
"@mantine/tiptap": ^9.2.2
"@next/eslint-plugin-next": 16.2.6
"@octokit/auth-app": ^8.2.0
"@paralleldrive/cuid2": ^3.3.0
@@ -43,12 +43,12 @@ catalog:
"@semantic-release/release-notes-generator": ^14.1.1
"@t3-oss/env-nextjs": ^0.13.11
"@tabler/icons-react": ^3.44.0
"@tanstack/react-query": ^5.100.13
"@tanstack/react-query-devtools": ^5.100.13
"@tanstack/react-query-next-experimental": ^5.100.13
"@testcontainers/mysql": ^12.0.0
"@testcontainers/postgresql": ^12.0.0
"@testcontainers/redis": ^12.0.0
"@tanstack/react-query": ^5.100.14
"@tanstack/react-query-devtools": ^5.100.14
"@tanstack/react-query-next-experimental": ^5.100.14
"@testcontainers/mysql": ^12.0.1
"@testcontainers/postgresql": ^12.0.1
"@testcontainers/redis": ^12.0.1
"@tiptap/extension-color": 3.23.6
"@tiptap/extension-highlight": 3.23.6
"@tiptap/extension-image": 3.23.6
@@ -70,7 +70,7 @@ catalog:
"@trpc/react-query": ^11.17.0
"@trpc/server": ^11.17.0
"@trpc/tanstack-react-query": ^11.17.0
"@turbo/gen": ^2.9.14
"@turbo/gen": ^2.9.16
"@types/adm-zip": 0.5.8
"@types/bcrypt": 6.0.0
"@types/better-sqlite3": 7.6.13
@@ -109,7 +109,7 @@ catalog:
croner: 10.0.1
cross-env: ^10.1.0
crypto-js: ^4.2.0
dayjs: ^1.11.20
dayjs: ^1.11.21
deepmerge: 4.3.1
dns-caching: ^0.2.9
dockerode: ^5.0.0
@@ -124,8 +124,8 @@ catalog:
flag-icons: ^7.5.0
glob: ^13.0.6
ical.js: ^2.2.1
ioredis: 5.10.1
isomorphic-dompurify: ^3.14.0
ioredis: 5.11.0
isomorphic-dompurify: ^3.15.0
jotai: ^2.20.0
jsdom: ^29.1.1
json5: ^2.2.3
@@ -133,21 +133,21 @@ catalog:
lodash.clonedeep: ^4.5.0
mantine-react-table: 2.0.0-beta.9
maria2: ^0.4.1
mysql2: 3.22.3
mysql2: 3.22.4
next: 16.2.6
next-auth: 5.0.0-beta.31
next-intl: 4.12.0
next-intl: 4.13.0
node-loader: ^2.1.0
octokit: ^5.0.5
oxfmt: ^0.51.0
oxlint: ^1.66.0
oxfmt: ^0.52.0
oxlint: ^1.67.0
pg: ^8.21.0
postcss-preset-mantine: ^1.18.0
prismjs: ^1.30.0
proxmox-api: 1.1.1
react: 19.2.6
react-dom: 19.2.6
react-error-boundary: ^6.1.1
react-error-boundary: ^6.1.2
react-markdown: ^10.1.0
react-quill-new: ^3.8.3
react-simple-code-editor: ^0.14.1
@@ -158,12 +158,12 @@ catalog:
superjson: 2.2.6
svgson: ^5.3.1
swagger-ui-react: ^5.32.6
testcontainers: ^12.0.0
testcontainers: ^12.0.1
timezones-ical-library: ^2.2.0
trpc-to-openapi: ^3.2.0
trpc-to-openapi: ^3.3.0
tsdav: ^2.2.2
tsx: 4.20.4
turbo: ^2.9.14
turbo: ^2.9.16
typescript: ^5.9.3
undici: 7.24.6
use-deep-compare-effect: ^1.8.1
+1 -1
View File
@@ -1 +1 @@
[{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"alotlikebeans","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontent.com/u/47495914?v=4"},{"login":"bebsworthy","avatar_url":"https://avatars.githubusercontent.cLine truncated
[{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"marektancak0106","avatar_url":"https://avatars.githubusercontent.com/u/25422667?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"alotlikebeans","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontLine truncated
+1 -1
View File
@@ -1 +1 @@
[{"username":"carlchina","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/6697/medium/d22bbe7797bbeb30dbdc73a5648d329a_default.png"},{"username":"zielmann","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png"},{"username":"Thalyn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12494028/medium/5faf5acc945a874ed116c4567edaa09f_default.png"},{"username":"magnushj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12503488/medium/68f32e33ea7b891960055c9f831f7344_default.png"},{"username":"almontegil","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12580457/medium/f4136cacbdfdb4c28ae7f85dc5f840db_default.png"},{"username":"andibing","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12664938/medium/b8be63e4dcb2e791ced1ffc9e3a049a5.jpg"},{"username":"gronare","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12680911/medium/c6eb14d1504156f34a158e507f73aa3d_default.png"},{"username":"SmartPhoneLover","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12701640/medium/ec95486662ec875cda080e778c3ff702.jpg"},{"username":"marcelotk_15","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12773227/medium/ecf6bea16ab0c873c131df2154ee9718_default.png"},{"username":"S3OD177","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13051544/medium/11dd990837f61f7e8f7d36b041a7fddb_default.png"},{"username":"marcg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13153301/medium/ebcf60fbf27deb148ec40a659566c6ff_default.png"},{"username":"BeersTeddy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13185230/medium/2f1f4e1effe74a23422b195cbefb2a95_default.png"},{"username":"jamesmcmahon0","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13245578/medium/586aa873b4abddbd9abc6f3de99ab70e.jpeg"},{"username":"b1nar10","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13286752/medium/249b02e3368752cb175446082015737c_default.png"},{"username":"vannCN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13330448/medium/9b8a9ee3611e51e951e22d5fd4eb7d8d.jpg"},{"username":"binswm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13343482/medium/45dde0e6097b9b72705d2eba9dbbc276_default.png"},{"username":"SkewRam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13496556/medium/188f5c2deb7938eda51eb786cc4539ca.jpeg"},{"username":"raphcatarino","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13547726/medium/b003511e67df13a4b4b5689488fa8099.jpg"},{"username":"shillos5","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13600557/medium/b17650b2b84f829d54fa548c44c1ab27.jpeg"},{"username":"wolong98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13641407/medium/f4634edc58c7857a357e5293543c15cf.jpg"},{"username":"spair0039","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14012333/medium/72430e96027c09c19141cac38eae4617.png"},{"username":"MajorMarcin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14072183/medium/0a197e64bb92b6ea94cecbb607512c45.jpg"},{"username":"KosmoMoustache","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14103535/medium/51c438cdd9f86e8811c75f4869de479c.png"},{"username":"Soochaehwa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14118689/medium/496a1ce63111547bf455a1e0a7ac75f1_default.png"},{"username":"garryfield","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14187035/medium/520c964706676045f6eeec1b08edc880.jpg"},{"username":"droidenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14444264/medium/4aa3a8e824f72bc9e5ec0d8de307095e.jpeg"},{"username":"maathis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14489950/medium/6eeddf13a757ee35f78f1763f94d95e1.jpeg"},{"username":"bukvam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14546502/medium/a819dc40a13c57395a17dc46e847d3ce.jpg"},{"username":"gm.cinalli","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14670666/medium/2d466a0fdbda40764526be86c97c0ab4.jpeg"},{"username":"antoine2tt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14722148/medium/f88d926900862dd59007ea4b3419cb9d.png"},{"username":"cretzen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14799754/medium/3ba5c1805ec20c000d5810c07985afe9.jpg"},{"username":"ktKongTong","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14817246/medium/97cfc3c028dbdaf85ebd1102da71e58c.jpeg"},{"username":"marinkaberg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14861042/medium/2d5c4e62613f03082f3e645fa92efd59.jpeg"},{"username":"djismgaming","avatarUrl":"httpsLine truncated
[{"username":"carlchina","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/6697/medium/d22bbe7797bbeb30dbdc73a5648d329a_default.png"},{"username":"zielmann","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png"},{"username":"Thalyn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12494028/medium/5faf5acc945a874ed116c4567edaa09f_default.png"},{"username":"magnushj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12503488/medium/68f32e33ea7b891960055c9f831f7344_default.png"},{"username":"almontegil","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12580457/medium/f4136cacbdfdb4c28ae7f85dc5f840db_default.png"},{"username":"andibing","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12664938/medium/b8be63e4dcb2e791ced1ffc9e3a049a5.jpg"},{"username":"gronare","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12680911/medium/c6eb14d1504156f34a158e507f73aa3d_default.png"},{"username":"SmartPhoneLover","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12701640/medium/ec95486662ec875cda080e778c3ff702.jpg"},{"username":"marcelotk_15","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12773227/medium/ecf6bea16ab0c873c131df2154ee9718_default.png"},{"username":"S3OD177","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13051544/medium/11dd990837f61f7e8f7d36b041a7fddb_default.png"},{"username":"marcg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13153301/medium/ebcf60fbf27deb148ec40a659566c6ff_default.png"},{"username":"BeersTeddy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13185230/medium/2f1f4e1effe74a23422b195cbefb2a95_default.png"},{"username":"jamesmcmahon0","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13245578/medium/586aa873b4abddbd9abc6f3de99ab70e.jpeg"},{"username":"b1nar10","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13286752/medium/249b02e3368752cb175446082015737c_default.png"},{"username":"vannCN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13330448/medium/9b8a9ee3611e51e951e22d5fd4eb7d8d.jpg"},{"username":"binswm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13343482/medium/45dde0e6097b9b72705d2eba9dbbc276_default.png"},{"username":"SkewRam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13496556/medium/188f5c2deb7938eda51eb786cc4539ca.jpeg"},{"username":"raphcatarino","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13547726/medium/b003511e67df13a4b4b5689488fa8099.jpg"},{"username":"shillos5","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13600557/medium/b17650b2b84f829d54fa548c44c1ab27.jpeg"},{"username":"wolong98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13641407/medium/f4634edc58c7857a357e5293543c15cf.jpg"},{"username":"spair0039","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14012333/medium/72430e96027c09c19141cac38eae4617.png"},{"username":"MajorMarcin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14072183/medium/0a197e64bb92b6ea94cecbb607512c45.jpg"},{"username":"KosmoMoustache","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14103535/medium/51c438cdd9f86e8811c75f4869de479c.png"},{"username":"Soochaehwa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14118689/medium/496a1ce63111547bf455a1e0a7ac75f1_default.png"},{"username":"garryfield","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14187035/medium/520c964706676045f6eeec1b08edc880.jpg"},{"username":"droidenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14444264/medium/4aa3a8e824f72bc9e5ec0d8de307095e.jpeg"},{"username":"maathis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14489950/medium/6eeddf13a757ee35f78f1763f94d95e1.jpeg"},{"username":"bukvam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14546502/medium/a819dc40a13c57395a17dc46e847d3ce.jpg"},{"username":"gm.cinalli","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14670666/medium/2d466a0fdbda40764526be86c97c0ab4.jpeg"},{"username":"antoine2tt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14722148/medium/f88d926900862dd59007ea4b3419cb9d.png"},{"username":"cretzen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14799754/medium/3ba5c1805ec20c000d5810c07985afe9.jpg"},{"username":"ktKongTong","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14817246/medium/97cfc3c028dbdaf85ebd1102da71e58c.jpeg"},{"username":"marinkaberg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14861042/medium/2d5c4e62613f03082f3e645fa92efd59.jpeg"},{"username":"djismgaming","avatarUrl":"httpsLine truncated