From ae66f8d89d0873fa3aa5549faa0d0f5536e5c79c Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sun, 31 May 2026 15:52:27 +0200 Subject: [PATCH] Feature: per-viewer colors for shared calendars (#345) --- app/(main)/[locale]/calendar/page.tsx | 74 +++++++++++++++--- .../calendar/calendar-sidebar-panel.tsx | 17 +++- components/calendar/event-card.tsx | 5 ++ .../settings/calendar-management-settings.tsx | 33 ++++---- lib/__tests__/shared-calendar-colors.test.ts | 77 +++++++++++++++++++ lib/jmap/types.ts | 4 + lib/shared-calendar-colors.ts | 55 +++++++++++++ locales/cs/common.json | 1 + locales/da/common.json | 1 + locales/de/common.json | 1 + locales/en/common.json | 1 + locales/es/common.json | 1 + locales/fr/common.json | 1 + locales/it/common.json | 1 + locales/ja/common.json | 1 + locales/ko/common.json | 1 + locales/lv/common.json | 1 + locales/nl/common.json | 1 + locales/pl/common.json | 1 + locales/pt/common.json | 1 + locales/ru/common.json | 1 + locales/tr/common.json | 1 + locales/uk/common.json | 1 + locales/zh/common.json | 1 + stores/settings-store.ts | 22 ++++++ 25 files changed, 272 insertions(+), 32 deletions(-) create mode 100644 lib/__tests__/shared-calendar-colors.test.ts create mode 100644 lib/shared-calendar-colors.ts diff --git a/app/(main)/[locale]/calendar/page.tsx b/app/(main)/[locale]/calendar/page.tsx index 80611f3f..edccc34f 100644 --- a/app/(main)/[locale]/calendar/page.tsx +++ b/app/(main)/[locale]/calendar/page.tsx @@ -60,6 +60,7 @@ import { useConfirmDialog } from "@/hooks/use-confirm-dialog"; import { CreateCalendarModal } from "@/components/calendar/create-calendar-modal"; import { getUserParticipantId } from "@/lib/calendar-participants"; import { generateBirthdayEvents, createBirthdayCalendar, BIRTHDAY_CALENDAR_ID } from "@/lib/birthday-calendar"; +import { sharedCalendarColorKey, pickUnusedCalendarColor } from "@/lib/shared-calendar-colors"; import { debug } from "@/lib/debug"; import { consumePendingWebcal, hasPendingWebcal, subscribeToPendingWebcal } from "@/lib/protocol-handlers/session"; import type { ParsedWebcal } from "@/lib/protocol-handlers/webcal"; @@ -97,6 +98,9 @@ export default function CalendarPage() { refreshAllSubscriptions, icalSubscriptions, } = useCalendarStore(); const { firstDayOfWeek, timeFormat, showWeekNumbers, enableCalendarTasks, showTasksOnCalendar, calendarHoverPreview, showBirthdayCalendar, birthdayCalendarColor, updateSetting } = useSettingsStore(); + const sharedCalendarColors = useSettingsStore((s) => s.sharedCalendarColors); + const setSharedCalendarColor = useSettingsStore((s) => s.setSharedCalendarColor); + const removeSharedCalendarColor = useSettingsStore((s) => s.removeSharedCalendarColor); const taskStore = useTaskStore(); const fetchTasksFn = useTaskStore(state => state.fetchTasks); const { identities } = useIdentityStore(); @@ -1030,10 +1034,47 @@ export default function CalendarPage() { try { return t('birthday_calendar'); } catch { return 'Birthdays'; } })(); + // Apply each shared calendar's local color override (per-viewer recolor, + // #345). The override replaces the calendar's color and wins over per-event + // colors via the `colorIsLocalOverride` flag (see getEventColor). Personal + // calendars are passed through untouched. + const displayCalendars = useMemo(() => { + return calendars.map((cal) => { + if (!cal.isShared) return cal; + const override = sharedCalendarColors[sharedCalendarColorKey(cal)]; + if (!override) return cal; + return { ...cal, color: override, colorIsLocalOverride: true }; + }); + }, [calendars, sharedCalendarColors]); + + // Auto-assign a random, not-yet-used palette color to any freshly shared + // calendar so multiple shared calendars don't collide on one color. Runs + // once per calendar (guarded by the presence of an existing key), and the + // user can still overwrite it from the sidebar. + useEffect(() => { + const shared = calendars.filter((c) => c.isShared); + const missing = shared.filter((c) => !sharedCalendarColors[sharedCalendarColorKey(c)]); + if (missing.length === 0) return; + // Seed "used" with personal calendar colors plus already-assigned shared + // overrides so the picks stay distinct from what's already on screen. + const used = new Set(); + for (const c of calendars) { + if (!c.isShared && c.color) used.add(c.color.toLowerCase()); + } + for (const color of Object.values(sharedCalendarColors)) { + if (color) used.add(color.toLowerCase()); + } + for (const cal of missing) { + const color = pickUnusedCalendarColor(used); + used.add(color.toLowerCase()); + setSharedCalendarColor(sharedCalendarColorKey(cal), color); + } + }, [calendars, sharedCalendarColors, setSharedCalendarColor]); + const allCalendars = useMemo(() => { - if (!showBirthdayCalendar) return calendars; - return [...calendars, createBirthdayCalendar(birthdayCalendarName, birthdayCalendarColor)]; - }, [calendars, showBirthdayCalendar, birthdayCalendarName, birthdayCalendarColor]); + if (!showBirthdayCalendar) return displayCalendars; + return [...displayCalendars, createBirthdayCalendar(birthdayCalendarName, birthdayCalendarColor)]; + }, [displayCalendars, showBirthdayCalendar, birthdayCalendarName, birthdayCalendarColor]); const visibleEvents = useMemo(() => { const filtered = events.filter((e) => { @@ -1219,7 +1260,7 @@ export default function CalendarPage() { /> c.id === calendarId); + if (cal?.isShared) { + setSharedCalendarColor(sharedCalendarColorKey(cal), color); + return; + } updateCalendar(client, calendarId, { color }); } : undefined} + onResetColor={(cal) => { + // Drop the local override; the auto-assign effect picks a + // fresh unused color (so it never reverts to a collision). + removeSharedCalendarColor(sharedCalendarColorKey(cal)); + }} onShareCalendar={client ? (cal) => setSharingCalendarId(cal.id) : undefined} onCreateEvent={(cal: Calendar) => { setDefaultCalendarIdForCreate(cal.id); @@ -1389,7 +1443,7 @@ export default function CalendarPage() { onSubscribe={() => setShowSubscriptionModal(true)} isMobile={isMobile} onNavigateBack={isMobile && mobileReturnToMonth && normalizedViewMode === "day" ? navigateBackToMonth : undefined} - calendars={calendars} + calendars={displayCalendars} selectedCalendarIds={selectedCalendarIds} onToggleVisibility={toggleCalendarVisibility} enableCalendarTasks={enableCalendarTasks} @@ -1419,7 +1473,7 @@ export default function CalendarPage() { { setShowTaskModal(false); setEditTask(null); }} @@ -1529,7 +1583,7 @@ export default function CalendarPage() { {detailEvent && detailAnchorRect && ( detailEvent.calendarIds[c.id])} + calendar={displayCalendars.find(c => detailEvent.calendarIds[c.id])} anchorRect={detailAnchorRect} onEdit={handleEditFromDetail} onDelete={handleDeleteFromDetail} @@ -1549,7 +1603,7 @@ export default function CalendarPage() { { diff --git a/components/calendar/calendar-sidebar-panel.tsx b/components/calendar/calendar-sidebar-panel.tsx index e93ab590..913e9bcb 100644 --- a/components/calendar/calendar-sidebar-panel.tsx +++ b/components/calendar/calendar-sidebar-panel.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from "react"; import { useTranslations } from "next-intl"; -import { ChevronDown, ChevronRight, Globe, ListTodo, Pencil, RefreshCw, Share2, Trash2, Cake, User, Users, Plus, Eraser, Palette } from "lucide-react"; +import { ChevronDown, ChevronRight, Globe, ListTodo, Pencil, RefreshCw, Share2, Trash2, Cake, User, Users, Plus, Eraser, Palette, Shuffle } from "lucide-react"; import { cn, formatDateTime } from "@/lib/utils"; import type { Calendar } from "@/lib/jmap/types"; import { CalendarColorPicker } from "@/components/settings/calendar-management-settings"; @@ -11,6 +11,7 @@ import { useSettingsStore } from "@/stores/settings-store"; import { useTaskStore } from "@/stores/task-store"; import { useAccountStore } from "@/stores/account-store"; import { BIRTHDAY_CALENDAR_ID } from "@/lib/birthday-calendar"; +import { sharedCalendarColorKey } from "@/lib/shared-calendar-colors"; import { toast } from "@/stores/toast-store"; import { ContextMenu, ContextMenuItem, ContextMenuSeparator, ContextMenuSubMenu } from "@/components/ui/context-menu"; import { useContextMenu } from "@/hooks/use-context-menu"; @@ -50,6 +51,7 @@ interface CalendarSidebarPanelProps { selectedCalendarIds: string[]; onToggleVisibility: (id: string) => void; onColorChange?: (calendarId: string, color: string) => void; + onResetColor?: (calendar: Calendar) => void; onShareCalendar?: (calendar: Calendar) => void; onCreateEvent?: (calendar: Calendar) => void; onClearCalendar?: (calendar: Calendar) => void; @@ -71,6 +73,7 @@ export function CalendarSidebarPanel({ selectedCalendarIds, onToggleVisibility, onColorChange, + onResetColor, onShareCalendar, onCreateEvent, onClearCalendar, @@ -94,6 +97,7 @@ export function CalendarSidebarPanel({ const refreshICalSubscription = useCalendarStore((s) => s.refreshICalSubscription); const removeICalSubscription = useCalendarStore((s) => s.removeICalSubscription); const timeFormat = useSettingsStore((s) => s.timeFormat); + const sharedCalendarColors = useSettingsStore((s) => s.sharedCalendarColors); const enableCalendarTasks = useSettingsStore((s) => s.enableCalendarTasks); const tasks = useTaskStore((s) => s.tasks); const setViewMode = useCalendarStore((s) => s.setViewMode); @@ -303,9 +307,11 @@ export function CalendarSidebarPanel({ const canCreate = onCreateEvent && !isBirthday && cal.myRights?.mayWriteOwn !== false; const canShare = onShareCalendar && cal.myRights?.mayShare && !cal.isShared; const canChangeColor = !!onColorChange; + const hasColorOverride = !!cal.isShared && !!sharedCalendarColors[sharedCalendarColorKey(cal)]; + const canResetColor = !!onResetColor && hasColorOverride; const canClear = onClearCalendar && !isBirthday && cal.myRights?.mayDelete !== false; const canDelete = onDeleteCalendar && !isBirthday && !cal.isDefault && !cal.isShared; - const showSeparator = (canCreate || canShare || canChangeColor) && (canClear || canDelete); + const showSeparator = (canCreate || canShare || canChangeColor || canResetColor) && (canClear || canDelete); const color = cal.color || "#3b82f6"; return ( @@ -335,6 +341,13 @@ export function CalendarSidebarPanel({ )} + {canResetColor && ( + { closeContextMenu(); onResetColor!(cal); }} + /> + )} {showSeparator && } {canClear && ( s.timeFormat); + const sharedCalendarColors = useSettingsStore((s) => s.sharedCalendarColors); + const setSharedCalendarColor = useSettingsStore((s) => s.setSharedCalendarColor); const colorPickerRef = useRef(null); // Load calendars if not yet loaded @@ -329,6 +313,15 @@ export function CalendarManagementSettings() { const handleColorChange = async (calendarId: string, color: string) => { if (!client) return; + // Shared calendars: recolor locally only - the viewer usually can't write + // the owner's calendar and it'd recolor it for everyone (see #345). + const cal = calendars.find((c) => c.id === calendarId); + if (cal?.isShared) { + setSharedCalendarColor(sharedCalendarColorKey(cal), color); + toast.success(t('color_updated')); + setColorPickerId(null); + return; + } try { await updateCalendar(client, calendarId, { color }); toast.success(t('color_updated')); @@ -396,7 +389,7 @@ export function CalendarManagementSettings() {
{calendars.filter(cal => !isSubscriptionCalendar(cal.id)).map((cal) => { - const color = cal.color || '#3b82f6'; + const color = (cal.isShared && sharedCalendarColors[sharedCalendarColorKey(cal)]) || cal.color || '#3b82f6'; if (editingId === cal.id) { return ( diff --git a/lib/__tests__/shared-calendar-colors.test.ts b/lib/__tests__/shared-calendar-colors.test.ts new file mode 100644 index 00000000..dedde6ed --- /dev/null +++ b/lib/__tests__/shared-calendar-colors.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect } from 'vitest'; +import type { Calendar } from '@/lib/jmap/types'; +import { + CALENDAR_COLORS, + sharedCalendarColorKey, + pickUnusedCalendarColor, +} from '../shared-calendar-colors'; + +function makeCal(overrides: Partial): Calendar { + return { + id: 'cal-1', + name: 'Cal', + description: null, + color: null, + sortOrder: 0, + isSubscribed: true, + isVisible: true, + isDefault: false, + includeInAvailability: 'all', + defaultAlertsWithTime: null, + defaultAlertsWithoutTime: null, + timeZone: null, + shareWith: null, + myRights: {} as Calendar['myRights'], + ...overrides, + }; +} + +describe('sharedCalendarColorKey', () => { + it('is built from local account, JMAP account, and the original id', () => { + const cal = makeCal({ + id: 'acct-9:cal-7', + originalId: 'cal-7', + accountId: 'acct-9', + localAccountId: 'slot-2', + }); + expect(sharedCalendarColorKey(cal)).toBe('slot-2|acct-9|cal-7'); + }); + + it('is stable regardless of the Pro-shell id prefix', () => { + // Same underlying calendar, shown once under the active account (bare id) + // and once cross-account (prefixed id) - both must map to one key. + const active = makeCal({ id: 'acct-9:cal-7', originalId: 'cal-7', accountId: 'acct-9', localAccountId: 'slot-2' }); + const prefixed = makeCal({ id: 'slot-2::acct-9:cal-7', originalId: 'cal-7', accountId: 'acct-9', localAccountId: 'slot-2' }); + expect(sharedCalendarColorKey(active)).toBe(sharedCalendarColorKey(prefixed)); + }); + + it('falls back to the id when originalId is absent', () => { + const cal = makeCal({ id: 'cal-7', accountId: 'acct-9' }); + expect(sharedCalendarColorKey(cal)).toBe('|acct-9|cal-7'); + }); +}); + +describe('pickUnusedCalendarColor', () => { + it('returns a palette color not present in usedColors', () => { + const used = CALENDAR_COLORS.slice(0, CALENDAR_COLORS.length - 1); + const picked = pickUnusedCalendarColor(used); + expect(picked).toBe(CALENDAR_COLORS[CALENDAR_COLORS.length - 1]); + }); + + it('ignores case when comparing used colors', () => { + const used = CALENDAR_COLORS.slice(0, -1).map((c) => c.toUpperCase()); + expect(pickUnusedCalendarColor(used)).toBe(CALENDAR_COLORS[CALENDAR_COLORS.length - 1]); + }); + + it('still returns a palette color once every color is taken', () => { + expect(CALENDAR_COLORS).toContain(pickUnusedCalendarColor(CALENDAR_COLORS)); + }); + + it('always returns a valid palette color for a small used set', () => { + for (let i = 0; i < 50; i++) { + const picked = pickUnusedCalendarColor(['#3b82f6']); + expect(CALENDAR_COLORS).toContain(picked); + expect(picked).not.toBe('#3b82f6'); + } + }); +}); diff --git a/lib/jmap/types.ts b/lib/jmap/types.ts index 3eee60c4..45356db4 100644 --- a/lib/jmap/types.ts +++ b/lib/jmap/types.ts @@ -488,6 +488,10 @@ export interface Calendar { // can route mutations to the right client. Distinct from `accountId` // which is the JMAP server's own account UUID. localAccountId?: string; + // Set when `color` has been replaced by the viewer's local override for a + // shared calendar (see lib/shared-calendar-colors). When true, the override + // wins over per-event colors so the whole shared calendar paints uniformly. + colorIsLocalOverride?: boolean; } export interface CalendarRights { diff --git a/lib/shared-calendar-colors.ts b/lib/shared-calendar-colors.ts new file mode 100644 index 00000000..9dea0c35 --- /dev/null +++ b/lib/shared-calendar-colors.ts @@ -0,0 +1,55 @@ +import type { Calendar } from '@/lib/jmap/types'; + +/** + * Palette of calendar colors offered in the color picker. Defined here (rather + * than in the settings UI component) so non-React modules can reuse it without + * pulling in component code. The settings color picker re-exports this. + */ +export const CALENDAR_COLORS = [ + "#3b82f6", // blue + "#ef4444", // red + "#22c55e", // green + "#f59e0b", // amber + "#8b5cf6", // violet + "#ec4899", // pink + "#14b8a6", // teal + "#f97316", // orange + "#06b6d4", // cyan + "#84cc16", // lime + "#6366f1", // indigo + "#a855f7", // purple + "#e11d48", // rose + "#0ea5e9", // sky + "#10b981", // emerald + "#d946ef", // fuchsia +]; + +/** + * Stable key for a shared calendar's local color override. Independent of the + * Pro-shell id prefix (which changes with the active account), so the override + * survives shell-mode and account switches. Built from the owning JMAP account + * + the calendar's original server id. + */ +export function sharedCalendarColorKey( + cal: Pick, +): string { + const localAccount = cal.localAccountId ?? ''; + const account = cal.accountId ?? ''; + const id = cal.originalId ?? cal.id; + return `${localAccount}|${account}|${id}`; +} + +/** + * Pick a random palette color not present in `usedColors`. Once every palette + * entry is taken, fall back to a random palette color (collisions are + * unavoidable past CALENDAR_COLORS.length calendars). + */ +export function pickUnusedCalendarColor(usedColors: Iterable): string { + const used = new Set(); + for (const c of usedColors) { + if (c) used.add(c.toLowerCase()); + } + const available = CALENDAR_COLORS.filter((c) => !used.has(c.toLowerCase())); + const pool = available.length > 0 ? available : CALENDAR_COLORS; + return pool[Math.floor(Math.random() * pool.length)]; +} diff --git a/locales/cs/common.json b/locales/cs/common.json index 96ff9d8b..72f4cc5b 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -2642,6 +2642,7 @@ "name_placeholder": "Název kalendáře", "color": "Barva", "change_color": "Změnit barvu", + "random_color": "Nová náhodná barva", "add_calendar": "Přidat kalendář", "edit": "Upravit", "delete": "Odstranit", diff --git a/locales/da/common.json b/locales/da/common.json index cec171f6..79788256 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -2656,6 +2656,7 @@ "name_placeholder": "Kalendernavn", "color": "Farve", "change_color": "Skift farve", + "random_color": "Ny tilfældig farve", "add_calendar": "Tilføj kalender", "edit": "Redigér", "delete": "Slet", diff --git a/locales/de/common.json b/locales/de/common.json index 2e3ce922..82bfa353 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -2642,6 +2642,7 @@ "name_placeholder": "Kalendername", "color": "Farbe", "change_color": "Farbe ändern", + "random_color": "Neue zufällige Farbe", "add_calendar": "Kalender hinzufügen", "edit": "Bearbeiten", "delete": "Löschen", diff --git a/locales/en/common.json b/locales/en/common.json index 21282898..4d9eb297 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -2657,6 +2657,7 @@ "name_placeholder": "Calendar name", "color": "Color", "change_color": "Change color", + "random_color": "New random color", "add_calendar": "Add calendar", "edit": "Edit", "delete": "Delete", diff --git a/locales/es/common.json b/locales/es/common.json index 24ae0fcf..d063bdb9 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -2642,6 +2642,7 @@ "name_placeholder": "Nombre del calendario", "color": "Color", "change_color": "Cambiar color", + "random_color": "Nuevo color aleatorio", "add_calendar": "Añadir calendario", "edit": "Editar", "delete": "Eliminar", diff --git a/locales/fr/common.json b/locales/fr/common.json index 1e619c5c..f4827d56 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -2656,6 +2656,7 @@ "name_placeholder": "Nom du calendrier", "color": "Couleur", "change_color": "Changer la couleur", + "random_color": "Nouvelle couleur aléatoire", "add_calendar": "Ajouter un calendrier", "edit": "Modifier", "delete": "Supprimer", diff --git a/locales/it/common.json b/locales/it/common.json index 6cde7370..45f30380 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -2642,6 +2642,7 @@ "name_placeholder": "Nome del calendario", "color": "Colore", "change_color": "Cambia colore", + "random_color": "Nuovo colore casuale", "add_calendar": "Aggiungi calendario", "edit": "Modifica", "delete": "Elimina", diff --git a/locales/ja/common.json b/locales/ja/common.json index 5c49d884..f41f86b5 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -2642,6 +2642,7 @@ "name_placeholder": "カレンダー名", "color": "色", "change_color": "色を変更", + "random_color": "新しいランダムな色", "add_calendar": "カレンダーを追加", "edit": "編集", "delete": "削除", diff --git a/locales/ko/common.json b/locales/ko/common.json index 74d2a880..4e736d55 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -2642,6 +2642,7 @@ "name_placeholder": "캘린더 이름", "color": "색상", "change_color": "색상 변경", + "random_color": "새 무작위 색상", "add_calendar": "캘린더 추가", "edit": "수정", "delete": "삭제", diff --git a/locales/lv/common.json b/locales/lv/common.json index 684b424d..2db8a856 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -2641,6 +2641,7 @@ "name_placeholder": "Kalendāra nosaukums", "color": "Krāsa", "change_color": "Mainīt krāsu", + "random_color": "Jauna nejauša krāsa", "add_calendar": "Pievienot kalendāru", "edit": "Rediģēt", "delete": "Dzēst", diff --git a/locales/nl/common.json b/locales/nl/common.json index 82ad8849..547e4f81 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -2642,6 +2642,7 @@ "name_placeholder": "Agendanaam", "color": "Kleur", "change_color": "Kleur wijzigen", + "random_color": "Nieuwe willekeurige kleur", "add_calendar": "Agenda toevoegen", "edit": "Bewerken", "delete": "Verwijderen", diff --git a/locales/pl/common.json b/locales/pl/common.json index 2ca08bc3..ac774fcf 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -2642,6 +2642,7 @@ "name_placeholder": "Nazwa kalendarza", "color": "Kolor", "change_color": "Zmień kolor", + "random_color": "Nowy losowy kolor", "add_calendar": "Dodaj kalendarz", "edit": "Edytuj", "delete": "Usuń", diff --git a/locales/pt/common.json b/locales/pt/common.json index 6a88b1ea..db998d81 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -2656,6 +2656,7 @@ "name_placeholder": "Nome do calendário", "color": "Cor", "change_color": "Alterar cor", + "random_color": "Nova cor aleatória", "add_calendar": "Adicionar calendário", "edit": "Editar", "delete": "Excluir", diff --git a/locales/ru/common.json b/locales/ru/common.json index afeae954..76bdb07c 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -2642,6 +2642,7 @@ "name_placeholder": "Название календаря", "color": "Цвет", "change_color": "Изменить цвет", + "random_color": "Новый случайный цвет", "add_calendar": "Добавить календарь", "edit": "Редактировать", "delete": "Удалить", diff --git a/locales/tr/common.json b/locales/tr/common.json index 7b2cd355..18d0c844 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -2656,6 +2656,7 @@ "name_placeholder": "Takvim adı", "color": "Renk", "change_color": "Rengi değiştir", + "random_color": "Yeni rastgele renk", "add_calendar": "Takvim ekle", "edit": "Düzenle", "delete": "Sil", diff --git a/locales/uk/common.json b/locales/uk/common.json index d04db44f..4d22f65d 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -2642,6 +2642,7 @@ "name_placeholder": "Назва календаря", "color": "колір", "change_color": "Змінити колір", + "random_color": "Новий випадковий колір", "add_calendar": "Додати календар", "edit": "Редагувати", "delete": "Видалити", diff --git a/locales/zh/common.json b/locales/zh/common.json index 62f75485..98e5cc9a 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -2642,6 +2642,7 @@ "name_placeholder": "日历名称", "color": "颜色", "change_color": "更改颜色", + "random_color": "随机新颜色", "add_calendar": "添加日历", "edit": "编辑", "delete": "删除", diff --git a/stores/settings-store.ts b/stores/settings-store.ts index 0db69a2b..4abfc3ef 100644 --- a/stores/settings-store.ts +++ b/stores/settings-store.ts @@ -183,6 +183,11 @@ interface SettingsState { showBirthdayCalendar: boolean; birthdayCalendarColor: string; + // Per-viewer color overrides for shared calendars, keyed by + // sharedCalendarColorKey(). Lets each recipient recolor calendars shared + // with them without changing the owner's color (see #345). + sharedCalendarColors: Record; + // Contacts Display groupContactsByLetter: boolean; @@ -274,6 +279,10 @@ interface SettingsState { setFolderIcon: (mailboxId: string, icon: string) => void; removeFolderIcon: (mailboxId: string) => void; + // Shared-calendar color overrides + setSharedCalendarColor: (key: string, color: string) => void; + removeSharedCalendarColor: (key: string) => void; + // Trusted senders addTrustedSender: (email: string) => void; removeTrustedSender: (email: string) => void; @@ -360,6 +369,8 @@ const DEFAULT_SETTINGS = { showBirthdayCalendar: false, birthdayCalendarColor: '#eab308', + sharedCalendarColors: {} as Record, + // Contacts Display groupContactsByLetter: true, @@ -546,6 +557,7 @@ export const useSettingsStore = create()( showTasksOnCalendar: state.showTasksOnCalendar, showBirthdayCalendar: state.showBirthdayCalendar, birthdayCalendarColor: state.birthdayCalendarColor, + sharedCalendarColors: state.sharedCalendarColors, groupContactsByLetter: state.groupContactsByLetter, expandedFilterView: state.expandedFilterView, showTimeInMonthView: state.showTimeInMonthView, @@ -642,6 +654,16 @@ export const useSettingsStore = create()( set({ folderIcons: rest }); }, + // Shared-calendar color override methods + setSharedCalendarColor: (key: string, color: string) => { + set({ sharedCalendarColors: { ...get().sharedCalendarColors, [key]: color } }); + }, + + removeSharedCalendarColor: (key: string) => { + const { [key]: _, ...rest } = get().sharedCalendarColors; + set({ sharedCalendarColors: rest }); + }, + // Trusted senders methods addTrustedSender: (email: string) => { const normalizedEmail = email.toLowerCase().trim();