mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: show a shared calendar in the color the server gives it #1414
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { CalendarSidebarPanel } from '../calendar-sidebar-panel';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import { sharedCalendarColorKey } from '@/lib/shared-calendar-colors';
|
||||
import type { IJMAPClient } from '@/lib/jmap/client-interface';
|
||||
import type { Calendar } from '@/lib/jmap/types';
|
||||
|
||||
// Dropping a shared calendar's own color brings back the server's color when
|
||||
// it has one (#1414), so the menu says so instead of promising a random one.
|
||||
|
||||
const family = {
|
||||
id: 'owner:family', originalId: 'family', accountId: 'owner', isShared: true,
|
||||
name: 'Family', color: '#22c55e', colorIsLocalOverride: true, sortOrder: 0,
|
||||
isSubscribed: true, isVisible: true, isDefault: false, includeInAvailability: 'all',
|
||||
timeZone: null, shareWith: null,
|
||||
myRights: { mayReadItems: true, mayWriteAll: false, mayWriteOwn: false, mayDelete: false },
|
||||
} as unknown as Calendar;
|
||||
|
||||
function openMenu(cal: Calendar, onResetColor: (cal: Calendar) => void) {
|
||||
const client = { getAccountId: () => 'me' } as unknown as IJMAPClient;
|
||||
render(
|
||||
<CalendarSidebarPanel
|
||||
calendars={[cal]}
|
||||
selectedCalendarIds={[cal.id]}
|
||||
onToggleVisibility={() => {}}
|
||||
onResetColor={onResetColor}
|
||||
client={client}
|
||||
/>,
|
||||
);
|
||||
fireEvent.contextMenu(screen.getByTestId('calendar-item'), { clientX: 20, clientY: 20 });
|
||||
}
|
||||
|
||||
describe('CalendarSidebarPanel shared calendar color reset', () => {
|
||||
beforeEach(() => {
|
||||
useSettingsStore.setState({ sharedCalendarColors: { [sharedCalendarColorKey(family)]: '#22c55e' } });
|
||||
});
|
||||
|
||||
it('offers to reset to the server color when the server has one', async () => {
|
||||
const onResetColor = vi.fn();
|
||||
openMenu({ ...family, serverColor: '#CD1526' }, onResetColor);
|
||||
const reset = await screen.findByRole('menuitem', { name: 'reset_color' });
|
||||
expect(screen.queryByRole('menuitem', { name: 'random_color' })).toBeNull();
|
||||
fireEvent.click(reset);
|
||||
expect(onResetColor).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('offers a new random color when the server has none', async () => {
|
||||
openMenu({ ...family, serverColor: null }, vi.fn());
|
||||
expect(await screen.findByRole('menuitem', { name: 'random_color' })).toBeTruthy();
|
||||
expect(screen.queryByRole('menuitem', { name: 'reset_color' })).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -79,7 +79,7 @@ import { getUserParticipantId, collectUserCalendarAddresses, userOrganizes, getP
|
||||
import type { MoveEventHandler } from "@/hooks/use-time-grid-interactions";
|
||||
import type { RevealTime } from "@/hooks/use-display-hours";
|
||||
import { generateBirthdayEvents, createBirthdayCalendar, BIRTHDAY_CALENDAR_ID } from "@/lib/birthday-calendar";
|
||||
import { sharedCalendarColorKey, pickUnusedCalendarColor } from "@/lib/shared-calendar-colors";
|
||||
import { sharedCalendarColorKey, autoAssignSharedCalendarColors } 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";
|
||||
@@ -1659,31 +1659,18 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
|
||||
if (!cal.isShared) return cal;
|
||||
const override = sharedCalendarColors[sharedCalendarColorKey(cal)];
|
||||
if (!override) return cal;
|
||||
return { ...cal, color: override, colorIsLocalOverride: true };
|
||||
return { ...cal, color: override, colorIsLocalOverride: true, serverColor: cal.color };
|
||||
});
|
||||
}, [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.
|
||||
// calendar the server gives no color, 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<string>();
|
||||
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);
|
||||
const assigned = autoAssignSharedCalendarColors(calendars, sharedCalendarColors);
|
||||
for (const [key, color] of Object.entries(assigned)) {
|
||||
setSharedCalendarColor(key, color);
|
||||
}
|
||||
}, [calendars, sharedCalendarColors, setSharedCalendarColor]);
|
||||
|
||||
@@ -2155,8 +2142,9 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
|
||||
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).
|
||||
// Drop the local override. A calendar with a server color goes
|
||||
// back to it; one without gets a fresh unused color from the
|
||||
// auto-assign effect (so it never reverts to a collision).
|
||||
removeSharedCalendarColor(sharedCalendarColorKey(cal));
|
||||
}}
|
||||
onShareCalendar={client ? (cal) => setSharingCalendarId(cal.id) : undefined}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useMemo, useRef, useState } from "react";
|
||||
import { useLongPress } from "@/hooks/use-long-press";
|
||||
import { IconButton } from "@/components/ui/icon-button";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Check, ChevronDown, ChevronRight, Globe, ListTodo, MoreVertical, Pencil, RefreshCw, Share2, Star, Trash2, Cake, User, Users, Plus, Eraser, Palette, Shuffle } from "@/components/icons";
|
||||
import { Check, ChevronDown, ChevronRight, Globe, ListTodo, MoreVertical, Pencil, RefreshCw, Share2, Star, Trash2, Cake, User, Users, Plus, Eraser, Palette, RotateCcw, Shuffle } from "@/components/icons";
|
||||
import { cn, formatDateTime } from "@/lib/utils";
|
||||
import type { Calendar } from "@/lib/jmap/types";
|
||||
import { CalendarColorPicker } from "@/components/settings/calendar-management-settings";
|
||||
@@ -441,8 +441,8 @@ export function CalendarSidebarPanel({
|
||||
)}
|
||||
{canResetColor && (
|
||||
<ContextMenuItem
|
||||
icon={Shuffle}
|
||||
label={tMgmt('random_color')}
|
||||
icon={cal.serverColor ? RotateCcw : Shuffle}
|
||||
label={tMgmt(cal.serverColor ? 'reset_color' : 'random_color')}
|
||||
onClick={() => { closeContextMenu(); onResetColor!(cal); }}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
CALENDAR_COLORS,
|
||||
sharedCalendarColorKey,
|
||||
pickUnusedCalendarColor,
|
||||
autoAssignSharedCalendarColors,
|
||||
} from '../shared-calendar-colors';
|
||||
|
||||
function makeCal(overrides: Partial<Calendar>): Calendar {
|
||||
@@ -75,3 +76,36 @@ describe('pickUnusedCalendarColor', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('autoAssignSharedCalendarColors', () => {
|
||||
const shared = (id: string, color: string | null) =>
|
||||
makeCal({ id, originalId: id, accountId: 'owner', isShared: true, color });
|
||||
|
||||
it('keeps the color the server gives a shared calendar (#1414)', () => {
|
||||
const family = shared('family', '#CD1526');
|
||||
expect(autoAssignSharedCalendarColors([family], {})).toEqual({});
|
||||
});
|
||||
|
||||
it('picks a palette color for a shared calendar without a server color', () => {
|
||||
const cal = shared('plain', null);
|
||||
const assigned = autoAssignSharedCalendarColors([cal], {});
|
||||
expect(Object.keys(assigned)).toEqual([sharedCalendarColorKey(cal)]);
|
||||
expect(CALENDAR_COLORS).toContain(assigned[sharedCalendarColorKey(cal)]);
|
||||
});
|
||||
|
||||
it('leaves calendars with a local override and personal calendars alone', () => {
|
||||
const overridden = shared('mine', null);
|
||||
const personal = makeCal({ id: 'own', color: null });
|
||||
const overrides = { [sharedCalendarColorKey(overridden)]: '#ef4444' };
|
||||
expect(autoAssignSharedCalendarColors([overridden, personal], overrides)).toEqual({});
|
||||
});
|
||||
|
||||
it('avoids colors already on screen, server colors of shared calendars included', () => {
|
||||
// Every palette color but the last is taken by a shared calendar's server
|
||||
// color, so the one calendar without a color must get the last one.
|
||||
const taken = CALENDAR_COLORS.slice(0, -1).map((color, i) => shared(`c${i}`, color));
|
||||
const plain = shared('plain', null);
|
||||
const assigned = autoAssignSharedCalendarColors([...taken, plain], {});
|
||||
expect(assigned).toEqual({ [sharedCalendarColorKey(plain)]: CALENDAR_COLORS[CALENDAR_COLORS.length - 1] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -661,6 +661,9 @@ export interface Calendar {
|
||||
// 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;
|
||||
// The server's own color, kept next to a local override so the sidebar can
|
||||
// tell whether dropping the override brings it back or picks a new one.
|
||||
serverColor?: string | null;
|
||||
// True when the calendar holds only tasks (VTODO) and no events, so it is
|
||||
// hidden from the event calendar UI while remaining available to the tasks
|
||||
// view (#761). Undefined means "not determined" (treated as not tasks-only).
|
||||
|
||||
@@ -53,3 +53,35 @@ export function pickUnusedCalendarColor(usedColors: Iterable<string>): string {
|
||||
const pool = available.length > 0 ? available : CALENDAR_COLORS;
|
||||
return pool[Math.floor(Math.random() * pool.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick an unused palette color for every shared calendar that has neither a
|
||||
* color from the server nor a local override, so several of them don't
|
||||
* collide on the default color. A shared calendar the server gives a color
|
||||
* keeps it until the viewer picks their own (#1414). Returns only the new
|
||||
* overrides, keyed by sharedCalendarColorKey().
|
||||
*/
|
||||
export function autoAssignSharedCalendarColors(
|
||||
calendars: Calendar[],
|
||||
overrides: Record<string, string>,
|
||||
): Record<string, string> {
|
||||
// Seed "used" with every color already on screen: personal and server
|
||||
// colors plus the existing shared overrides.
|
||||
const used = new Set<string>();
|
||||
for (const c of calendars) {
|
||||
if (c.color) used.add(c.color.toLowerCase());
|
||||
}
|
||||
for (const color of Object.values(overrides)) {
|
||||
if (color) used.add(color.toLowerCase());
|
||||
}
|
||||
const assigned: Record<string, string> = {};
|
||||
for (const cal of calendars) {
|
||||
if (!cal.isShared || cal.color) continue;
|
||||
const key = sharedCalendarColorKey(cal);
|
||||
if (overrides[key] || assigned[key]) continue;
|
||||
const color = pickUnusedCalendarColor(used);
|
||||
used.add(color.toLowerCase());
|
||||
assigned[key] = color;
|
||||
}
|
||||
return assigned;
|
||||
}
|
||||
|
||||
@@ -4146,6 +4146,7 @@
|
||||
"color": "اللون",
|
||||
"change_color": "تغيير اللون",
|
||||
"random_color": "لون عشوائي جديد",
|
||||
"reset_color": "إعادة تعيين اللون",
|
||||
"add_calendar": "تقويم جديد",
|
||||
"edit": "تعديل",
|
||||
"delete": "حذف",
|
||||
|
||||
@@ -4146,6 +4146,7 @@
|
||||
"color": "Color",
|
||||
"change_color": "Canvia el color",
|
||||
"random_color": "Color aleatori nou",
|
||||
"reset_color": "Restableix el color",
|
||||
"add_calendar": "Calendari nou",
|
||||
"edit": "Edita",
|
||||
"delete": "Suprimeix",
|
||||
|
||||
@@ -4117,6 +4117,7 @@
|
||||
"color": "Barva",
|
||||
"change_color": "Změnit barvu",
|
||||
"random_color": "Nová náhodná barva",
|
||||
"reset_color": "Obnovit barvu",
|
||||
"add_calendar": "Nový kalendář",
|
||||
"edit": "Upravit",
|
||||
"delete": "Odstranit",
|
||||
|
||||
@@ -4143,6 +4143,7 @@
|
||||
"color": "Farve",
|
||||
"change_color": "Skift farve",
|
||||
"random_color": "Ny tilfældig farve",
|
||||
"reset_color": "Nulstil farve",
|
||||
"add_calendar": "Ny kalender",
|
||||
"edit": "Redigér",
|
||||
"delete": "Slet",
|
||||
|
||||
@@ -4117,6 +4117,7 @@
|
||||
"color": "Farbe",
|
||||
"change_color": "Farbe ändern",
|
||||
"random_color": "Neue zufällige Farbe",
|
||||
"reset_color": "Farbe zurücksetzen",
|
||||
"add_calendar": "Neuer Kalender",
|
||||
"edit": "Bearbeiten",
|
||||
"delete": "Löschen",
|
||||
|
||||
@@ -4146,6 +4146,7 @@
|
||||
"color": "Color",
|
||||
"change_color": "Change color",
|
||||
"random_color": "New random color",
|
||||
"reset_color": "Reset color",
|
||||
"add_calendar": "New calendar",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
|
||||
@@ -4117,6 +4117,7 @@
|
||||
"color": "Color",
|
||||
"change_color": "Cambiar color",
|
||||
"random_color": "Nuevo color aleatorio",
|
||||
"reset_color": "Restablecer color",
|
||||
"add_calendar": "Nuevo calendario",
|
||||
"edit": "Editar",
|
||||
"delete": "Eliminar",
|
||||
|
||||
@@ -4146,6 +4146,7 @@
|
||||
"color": "Värv",
|
||||
"change_color": "Muuda värvi",
|
||||
"random_color": "Uus juhuslik värv",
|
||||
"reset_color": "Lähtesta värv",
|
||||
"add_calendar": "Uus kalender",
|
||||
"edit": "Muuda",
|
||||
"delete": "Kustuta",
|
||||
|
||||
@@ -4146,6 +4146,7 @@
|
||||
"color": "رنگ",
|
||||
"change_color": "تغییر رنگ",
|
||||
"random_color": "رنگ تصادفی جدید",
|
||||
"reset_color": "بازنشانی رنگ",
|
||||
"add_calendar": "تقویم جدید",
|
||||
"edit": "ویرایش",
|
||||
"delete": "حذف",
|
||||
|
||||
@@ -4143,6 +4143,7 @@
|
||||
"color": "Couleur",
|
||||
"change_color": "Changer la couleur",
|
||||
"random_color": "Nouvelle couleur aléatoire",
|
||||
"reset_color": "Réinitialiser la couleur",
|
||||
"add_calendar": "Nouveau calendrier",
|
||||
"edit": "Modifier",
|
||||
"delete": "Supprimer",
|
||||
|
||||
@@ -4049,6 +4049,7 @@
|
||||
"copy_url": "העתק כתובת URL של CalDAV",
|
||||
"url_copied": "כתובת האתר של CalDAV הועתקה ללוח",
|
||||
"random_color": "צבע אקראי חדש",
|
||||
"reset_color": "איפוס צבע",
|
||||
"set_default": "הגדר כברירת מחדל",
|
||||
"default_updated": "לוח שנה ברירת המחדל עודכן",
|
||||
"error_default": "נכשל בהגדרת לוח שנה ברירת מחדל",
|
||||
|
||||
@@ -4146,6 +4146,7 @@
|
||||
"color": "Szín",
|
||||
"change_color": "Szín módosítása",
|
||||
"random_color": "Véletlenszerű szín",
|
||||
"reset_color": "Szín visszaállítása",
|
||||
"add_calendar": "Új naptár",
|
||||
"edit": "Szerkesztés",
|
||||
"delete": "Törlés",
|
||||
|
||||
@@ -4117,6 +4117,7 @@
|
||||
"color": "Colore",
|
||||
"change_color": "Cambia colore",
|
||||
"random_color": "Nuovo colore casuale",
|
||||
"reset_color": "Ripristina colore",
|
||||
"add_calendar": "Nuovo calendario",
|
||||
"edit": "Modifica",
|
||||
"delete": "Elimina",
|
||||
|
||||
@@ -4117,6 +4117,7 @@
|
||||
"color": "色",
|
||||
"change_color": "色を変更",
|
||||
"random_color": "新しいランダムな色",
|
||||
"reset_color": "色をリセット",
|
||||
"add_calendar": "新しいカレンダー",
|
||||
"edit": "編集",
|
||||
"delete": "削除",
|
||||
|
||||
@@ -4117,6 +4117,7 @@
|
||||
"color": "색상",
|
||||
"change_color": "색상 변경",
|
||||
"random_color": "새 무작위 색상",
|
||||
"reset_color": "색상 재설정",
|
||||
"add_calendar": "새 캘린더",
|
||||
"edit": "수정",
|
||||
"delete": "삭제",
|
||||
|
||||
@@ -4116,6 +4116,7 @@
|
||||
"color": "Krāsa",
|
||||
"change_color": "Mainīt krāsu",
|
||||
"random_color": "Jauna nejauša krāsa",
|
||||
"reset_color": "Atiestatīt krāsu",
|
||||
"add_calendar": "Jauns kalendārs",
|
||||
"edit": "Rediģēt",
|
||||
"delete": "Dzēst",
|
||||
|
||||
@@ -4146,6 +4146,7 @@
|
||||
"color": "Өнгө",
|
||||
"change_color": "Өнгийг өөрчлөх",
|
||||
"random_color": "Шинэ санамсаргүй өнгө",
|
||||
"reset_color": "Өнгийг сэргээх",
|
||||
"add_calendar": "Шинэ хуанли",
|
||||
"edit": "Засварлах",
|
||||
"delete": "Устгах",
|
||||
|
||||
@@ -4146,6 +4146,7 @@
|
||||
"color": "Farge",
|
||||
"change_color": "Endre farge",
|
||||
"random_color": "Ny tilfeldig farge",
|
||||
"reset_color": "Tilbakestill farge",
|
||||
"add_calendar": "Ny kalender",
|
||||
"edit": "Rediger",
|
||||
"delete": "Slett",
|
||||
|
||||
@@ -4117,6 +4117,7 @@
|
||||
"color": "Kleur",
|
||||
"change_color": "Kleur wijzigen",
|
||||
"random_color": "Nieuwe willekeurige kleur",
|
||||
"reset_color": "Kleur herstellen",
|
||||
"add_calendar": "Nieuwe agenda",
|
||||
"edit": "Bewerken",
|
||||
"delete": "Verwijderen",
|
||||
|
||||
@@ -4117,6 +4117,7 @@
|
||||
"color": "Kolor",
|
||||
"change_color": "Zmień kolor",
|
||||
"random_color": "Nowy losowy kolor",
|
||||
"reset_color": "Przywróć kolor",
|
||||
"add_calendar": "Nowy kalendarz",
|
||||
"edit": "Edytuj",
|
||||
"delete": "Usuń",
|
||||
|
||||
@@ -4143,6 +4143,7 @@
|
||||
"color": "Cor",
|
||||
"change_color": "Alterar cor",
|
||||
"random_color": "Nova cor aleatória",
|
||||
"reset_color": "Repor cor",
|
||||
"add_calendar": "Novo calendário",
|
||||
"edit": "Editar",
|
||||
"delete": "Excluir",
|
||||
|
||||
@@ -4146,6 +4146,7 @@
|
||||
"color": "Culoare",
|
||||
"change_color": "Schimbă culoarea",
|
||||
"random_color": "Culoare aleatorie nouă",
|
||||
"reset_color": "Resetează culoarea",
|
||||
"add_calendar": "Calendar nou",
|
||||
"edit": "Editare",
|
||||
"delete": "Șterge",
|
||||
|
||||
@@ -4117,6 +4117,7 @@
|
||||
"color": "Цвет",
|
||||
"change_color": "Изменить цвет",
|
||||
"random_color": "Новый случайный цвет",
|
||||
"reset_color": "Сбросить цвет",
|
||||
"add_calendar": "Новый календарь",
|
||||
"edit": "Редактировать",
|
||||
"delete": "Удалить",
|
||||
|
||||
@@ -4146,6 +4146,7 @@
|
||||
"color": "Farba",
|
||||
"change_color": "Zmeniť farbu",
|
||||
"random_color": "Nová náhodná farba",
|
||||
"reset_color": "Obnoviť farbu",
|
||||
"add_calendar": "Nový kalendár",
|
||||
"edit": "Upraviť",
|
||||
"delete": "Odstrániť",
|
||||
|
||||
@@ -3398,6 +3398,7 @@
|
||||
"color": "สี",
|
||||
"change_color": "เปลี่ยนสี",
|
||||
"random_color": "สุ่มสีใหม่",
|
||||
"reset_color": "รีเซ็ตสี",
|
||||
"add_calendar": "ปฏิทินใหม่",
|
||||
"edit": "แก้ไข",
|
||||
"delete": "ลบ",
|
||||
|
||||
@@ -4143,6 +4143,7 @@
|
||||
"color": "Renk",
|
||||
"change_color": "Rengi değiştir",
|
||||
"random_color": "Yeni rastgele renk",
|
||||
"reset_color": "Rengi sıfırla",
|
||||
"add_calendar": "Yeni takvim",
|
||||
"edit": "Düzenle",
|
||||
"delete": "Sil",
|
||||
|
||||
@@ -4117,6 +4117,7 @@
|
||||
"color": "колір",
|
||||
"change_color": "Змінити колір",
|
||||
"random_color": "Новий випадковий колір",
|
||||
"reset_color": "Скинути колір",
|
||||
"add_calendar": "Новий календар",
|
||||
"edit": "Редагувати",
|
||||
"delete": "Видалити",
|
||||
|
||||
@@ -4146,6 +4146,7 @@
|
||||
"color": "顏色",
|
||||
"change_color": "變更顏色",
|
||||
"random_color": "隨機新顏色",
|
||||
"reset_color": "重設顏色",
|
||||
"add_calendar": "新增行事曆",
|
||||
"edit": "編輯",
|
||||
"delete": "刪除",
|
||||
|
||||
@@ -4117,6 +4117,7 @@
|
||||
"color": "颜色",
|
||||
"change_color": "更改颜色",
|
||||
"random_color": "随机新颜色",
|
||||
"reset_color": "重置颜色",
|
||||
"add_calendar": "新建日历",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
|
||||
Reference in New Issue
Block a user