mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2270602a3 | |||
| 8b1e5b2ee7 |
@@ -0,0 +1,748 @@
|
||||
import type { Plugin } from "../../core/types";
|
||||
import { booleanSetting } from "@/plugins/core/settingsHelpers";
|
||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||
import styles from "./styles.css?inline";
|
||||
|
||||
const messageFoldersSettings = {
|
||||
showTagsInAllMessages: booleanSetting({
|
||||
default: true,
|
||||
title: "Show folder tags in All Messages",
|
||||
description:
|
||||
"When off, folder tags are not shown on the message list until you select a folder.",
|
||||
}),
|
||||
hideFolderedMessagesInAll: booleanSetting({
|
||||
default: true,
|
||||
title: "Hide foldered messages in All Messages",
|
||||
description:
|
||||
"When on, messages assigned to a custom folder are hidden from the inbox until you open that folder.",
|
||||
}),
|
||||
} as const;
|
||||
|
||||
interface Folder {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface MessageFoldersStorage {
|
||||
folders: Folder[];
|
||||
messageAssignments: Record<string, string[]>;
|
||||
}
|
||||
|
||||
const FOLDER_COLORS = [
|
||||
"#3b82f6", "#ef4444", "#22c55e", "#f59e0b",
|
||||
"#8b5cf6", "#ec4899", "#14b8a6", "#f97316",
|
||||
];
|
||||
|
||||
const FOLDER_ICON_SVG = `<svg style="width:24px;height:24px;flex-shrink:0" viewBox="0 0 24 24"><path fill="#888" d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>`;
|
||||
const PLUS_SVG = `<svg style="width:14px;height:14px;flex-shrink:0" viewBox="0 0 24 24"><path fill="#888" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>`;
|
||||
const CHECK_SVG_WHITE = `<svg style="width:14px;height:14px;flex-shrink:0" viewBox="0 0 24 24"><path fill="#fff" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/></svg>`;
|
||||
const CLOSE_SVG = `<svg style="width:14px;height:14px;flex-shrink:0" viewBox="0 0 24 24"><path fill="#888" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>`;
|
||||
const EDIT_SVG = `<svg style="width:12px;height:12px;flex-shrink:0" viewBox="0 0 24 24"><path fill="#888" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 0 0 0-1.41l-2.34-2.34a1 1 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>`;
|
||||
const TRASH_SVG = `<svg style="width:12px;height:12px;flex-shrink:0" viewBox="0 0 24 24"><path fill="#888" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>`;
|
||||
|
||||
function generateId(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
||||
}
|
||||
|
||||
const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFoldersStorage> = {
|
||||
id: "messageFolders",
|
||||
name: "Message Folders",
|
||||
description: "Organize direct messages into custom folders",
|
||||
version: "1.0.0",
|
||||
settings: messageFoldersSettings,
|
||||
disableToggle: true,
|
||||
defaultEnabled: true,
|
||||
|
||||
run: async (api) => {
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.textContent = styles;
|
||||
document.head.appendChild(styleEl);
|
||||
|
||||
await api.storage.loaded;
|
||||
|
||||
if (!api.storage.folders) api.storage.folders = [];
|
||||
if (!api.storage.messageAssignments) api.storage.messageAssignments = {};
|
||||
|
||||
let activeFolderId: string | null = null;
|
||||
let messageListObserver: MutationObserver | null = null;
|
||||
let sidebarObserver: MutationObserver | null = null;
|
||||
let actionsObserver: MutationObserver | null = null;
|
||||
let openDropdown: HTMLElement | null = null;
|
||||
let dropdownCloseHandler: ((e: MouseEvent) => void) | null = null;
|
||||
const unregisters: Array<{ unregister: () => void }> = [];
|
||||
|
||||
// ── Storage accessors ──
|
||||
|
||||
const getFolders = (): Folder[] => api.storage.folders ?? [];
|
||||
const getAssignments = (): Record<string, string[]> => api.storage.messageAssignments ?? {};
|
||||
|
||||
const saveFolders = (folders: Folder[]) => {
|
||||
api.storage.folders = [...folders];
|
||||
};
|
||||
|
||||
const saveAssignments = (assignments: Record<string, string[]>) => {
|
||||
api.storage.messageAssignments = { ...assignments };
|
||||
};
|
||||
|
||||
const getMessageFolderIds = (messageId: string): string[] => {
|
||||
const assignments = getAssignments();
|
||||
const ids: string[] = [];
|
||||
for (const [folderId, msgIds] of Object.entries(assignments)) {
|
||||
if (msgIds.includes(messageId)) ids.push(folderId);
|
||||
}
|
||||
return ids;
|
||||
};
|
||||
|
||||
const toggleMessageInFolder = (messageId: string, folderId: string) => {
|
||||
const assignments = getAssignments();
|
||||
if (!assignments[folderId]) assignments[folderId] = [];
|
||||
const idx = assignments[folderId].indexOf(messageId);
|
||||
if (idx >= 0) {
|
||||
assignments[folderId].splice(idx, 1);
|
||||
} else {
|
||||
assignments[folderId].push(messageId);
|
||||
}
|
||||
saveAssignments(assignments);
|
||||
};
|
||||
|
||||
const getFolderMessageCount = (folderId: string): number => {
|
||||
return (getAssignments()[folderId] ?? []).length;
|
||||
};
|
||||
|
||||
const restoreSubjectPlain = (subject: Element) => {
|
||||
subject.querySelector(".bsplus-msg-badges")?.remove();
|
||||
const textWrap = subject.querySelector(".bsplus-subject-text");
|
||||
if (textWrap) {
|
||||
subject.textContent = textWrap.textContent ?? "";
|
||||
}
|
||||
};
|
||||
|
||||
const isMessageInAnyCustomFolder = (messageId: string): boolean => {
|
||||
for (const msgIds of Object.values(getAssignments())) {
|
||||
if (msgIds.includes(messageId)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const shouldShowBadgesInList = (): boolean => {
|
||||
return api.settings.showTagsInAllMessages || activeFolderId !== null;
|
||||
};
|
||||
|
||||
// ── Confirm modal ──
|
||||
|
||||
const showConfirmModal = (
|
||||
title: string,
|
||||
message: string,
|
||||
onConfirm: () => void,
|
||||
) => {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "bsplus-modal-overlay";
|
||||
|
||||
const modal = document.createElement("div");
|
||||
modal.className = "bsplus-modal";
|
||||
modal.innerHTML = `
|
||||
<h3>${title}</h3>
|
||||
<p>${message}</p>
|
||||
<div class="bsplus-modal-actions">
|
||||
<button class="bsplus-modal-btn-cancel">Cancel</button>
|
||||
<button class="bsplus-modal-btn-danger">Delete</button>
|
||||
</div>
|
||||
`;
|
||||
overlay.appendChild(modal);
|
||||
|
||||
const remove = () => {
|
||||
overlay.remove();
|
||||
document.removeEventListener("keydown", onKey);
|
||||
};
|
||||
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") remove();
|
||||
};
|
||||
|
||||
overlay.addEventListener("click", (e) => {
|
||||
if (e.target === overlay) remove();
|
||||
});
|
||||
modal.querySelector(".bsplus-modal-btn-cancel")!.addEventListener("click", remove);
|
||||
modal.querySelector(".bsplus-modal-btn-danger")!.addEventListener("click", () => {
|
||||
onConfirm();
|
||||
remove();
|
||||
});
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
document.addEventListener("keydown", onKey);
|
||||
};
|
||||
|
||||
// ── Sidebar folder UI ──
|
||||
|
||||
const renderSidebarFolders = () => {
|
||||
const sidebar = document.querySelector("[class*='Viewer__sidebar___']");
|
||||
if (!sidebar) return;
|
||||
|
||||
const ol = sidebar.querySelector("ol");
|
||||
if (!ol) return;
|
||||
|
||||
let section = ol.querySelector(".bsplus-folders-section");
|
||||
if (!section) {
|
||||
section = document.createElement("div");
|
||||
section.className = "bsplus-folders-section";
|
||||
ol.appendChild(section);
|
||||
}
|
||||
|
||||
const folders = getFolders();
|
||||
const existingInput = section.querySelector(".bsplus-folder-input");
|
||||
const existingColors = section.querySelector(".bsplus-folder-colors");
|
||||
|
||||
section.innerHTML = "";
|
||||
|
||||
// Header
|
||||
const header = document.createElement("div");
|
||||
header.className = "bsplus-folders-header";
|
||||
|
||||
const label = document.createElement("span");
|
||||
label.textContent = "Folders";
|
||||
header.appendChild(label);
|
||||
|
||||
const addBtn = document.createElement("button");
|
||||
addBtn.className = "bsplus-folders-add-btn";
|
||||
addBtn.title = "New folder";
|
||||
addBtn.innerHTML = PLUS_SVG;
|
||||
addBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
showNewFolderInput(section!);
|
||||
});
|
||||
header.appendChild(addBtn);
|
||||
section.appendChild(header);
|
||||
|
||||
// "All Messages" item
|
||||
const allItem = document.createElement("div");
|
||||
allItem.className = `bsplus-folder-item${activeFolderId === null ? " bsplus-folder-active" : ""}`;
|
||||
allItem.innerHTML = `
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" style="fill: currentcolor; opacity: 0.5; flex-shrink: 0;"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>
|
||||
<span class="bsplus-folder-name">All Messages</span>
|
||||
`;
|
||||
allItem.addEventListener("click", () => {
|
||||
activeFolderId = null;
|
||||
applyFolderFilter();
|
||||
applyBadges();
|
||||
renderSidebarFolders();
|
||||
});
|
||||
section.appendChild(allItem);
|
||||
|
||||
// Folder items
|
||||
for (const folder of folders) {
|
||||
const item = document.createElement("div");
|
||||
item.className = `bsplus-folder-item${activeFolderId === folder.id ? " bsplus-folder-active" : ""}`;
|
||||
item.dataset.folderId = folder.id;
|
||||
|
||||
const dot = document.createElement("div");
|
||||
dot.className = "bsplus-folder-dot";
|
||||
dot.style.background = folder.color;
|
||||
item.appendChild(dot);
|
||||
|
||||
const name = document.createElement("span");
|
||||
name.className = "bsplus-folder-name";
|
||||
name.textContent = folder.name;
|
||||
item.appendChild(name);
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "bsplus-folder-actions";
|
||||
|
||||
const editBtn = document.createElement("button");
|
||||
editBtn.className = "bsplus-folder-action-btn";
|
||||
editBtn.title = "Rename";
|
||||
editBtn.innerHTML = EDIT_SVG;
|
||||
editBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
showEditFolderInput(section!, folder);
|
||||
});
|
||||
actions.appendChild(editBtn);
|
||||
|
||||
const deleteBtn = document.createElement("button");
|
||||
deleteBtn.className = "bsplus-folder-action-btn";
|
||||
deleteBtn.title = "Delete";
|
||||
deleteBtn.innerHTML = TRASH_SVG;
|
||||
deleteBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
showConfirmModal(
|
||||
"Delete folder",
|
||||
`Remove "${folder.name}"? Messages won't be deleted.`,
|
||||
() => {
|
||||
const folders = getFolders().filter((f) => f.id !== folder.id);
|
||||
saveFolders(folders);
|
||||
const assignments = getAssignments();
|
||||
delete assignments[folder.id];
|
||||
saveAssignments(assignments);
|
||||
if (activeFolderId === folder.id) activeFolderId = null;
|
||||
applyFolderFilter();
|
||||
applyBadges();
|
||||
renderSidebarFolders();
|
||||
},
|
||||
);
|
||||
});
|
||||
actions.appendChild(deleteBtn);
|
||||
|
||||
item.appendChild(actions);
|
||||
|
||||
const count = document.createElement("span");
|
||||
count.className = "bsplus-folder-count";
|
||||
const c = getFolderMessageCount(folder.id);
|
||||
count.textContent = c > 0 ? String(c) : "";
|
||||
item.appendChild(count);
|
||||
|
||||
item.addEventListener("click", () => {
|
||||
activeFolderId = folder.id;
|
||||
applyFolderFilter();
|
||||
applyBadges();
|
||||
renderSidebarFolders();
|
||||
});
|
||||
|
||||
section.appendChild(item);
|
||||
}
|
||||
|
||||
// Restore input if it was open
|
||||
if (existingInput || existingColors) {
|
||||
// Don't restore – let user re-trigger
|
||||
}
|
||||
};
|
||||
|
||||
const showNewFolderInput = (container: Element, editFolder?: Folder) => {
|
||||
const existing = container.querySelector(".bsplus-folder-input");
|
||||
if (existing) existing.remove();
|
||||
container.querySelector(".bsplus-folder-colors")?.remove();
|
||||
|
||||
let selectedColor = editFolder?.color ?? FOLDER_COLORS[Math.floor(Math.random() * FOLDER_COLORS.length)];
|
||||
|
||||
const row = document.createElement("div");
|
||||
row.className = "bsplus-folder-input";
|
||||
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.placeholder = editFolder ? "Rename folder…" : "Folder name…";
|
||||
input.value = editFolder?.name ?? "";
|
||||
input.maxLength = 30;
|
||||
|
||||
const confirmBtn = document.createElement("button");
|
||||
confirmBtn.className = "bsplus-folder-input-confirm";
|
||||
confirmBtn.innerHTML = CHECK_SVG_WHITE;
|
||||
|
||||
const cancelBtn = document.createElement("button");
|
||||
cancelBtn.className = "bsplus-folder-input-cancel";
|
||||
cancelBtn.innerHTML = CLOSE_SVG;
|
||||
|
||||
row.appendChild(input);
|
||||
row.appendChild(confirmBtn);
|
||||
row.appendChild(cancelBtn);
|
||||
|
||||
// Color picker
|
||||
const colorRow = document.createElement("div");
|
||||
colorRow.className = "bsplus-folder-colors";
|
||||
for (const color of FOLDER_COLORS) {
|
||||
const swatch = document.createElement("button");
|
||||
swatch.className = `bsplus-folder-color-opt${color === selectedColor ? " bsplus-color-selected" : ""}`;
|
||||
swatch.style.background = color;
|
||||
swatch.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
selectedColor = color;
|
||||
colorRow.querySelectorAll(".bsplus-folder-color-opt").forEach((s) =>
|
||||
s.classList.toggle("bsplus-color-selected", (s as HTMLElement).style.background === color),
|
||||
);
|
||||
});
|
||||
colorRow.appendChild(swatch);
|
||||
}
|
||||
|
||||
const confirm = () => {
|
||||
const name = input.value.trim();
|
||||
if (!name) return;
|
||||
|
||||
if (editFolder) {
|
||||
const folders = getFolders().map((f) =>
|
||||
f.id === editFolder.id ? { ...f, name, color: selectedColor } : f,
|
||||
);
|
||||
saveFolders(folders);
|
||||
} else {
|
||||
const folder: Folder = { id: generateId(), name, color: selectedColor };
|
||||
saveFolders([...getFolders(), folder]);
|
||||
}
|
||||
applyBadges();
|
||||
renderSidebarFolders();
|
||||
};
|
||||
|
||||
confirmBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
confirm();
|
||||
});
|
||||
cancelBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
renderSidebarFolders();
|
||||
});
|
||||
input.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") confirm();
|
||||
if (e.key === "Escape") renderSidebarFolders();
|
||||
});
|
||||
|
||||
container.appendChild(row);
|
||||
container.appendChild(colorRow);
|
||||
requestAnimationFrame(() => input.focus());
|
||||
};
|
||||
|
||||
const showEditFolderInput = (container: Element, folder: Folder) => {
|
||||
showNewFolderInput(container, folder);
|
||||
};
|
||||
|
||||
// ── Intercept native sidebar clicks to clear folder filter ──
|
||||
|
||||
const attachNativeSidebarListeners = () => {
|
||||
const sidebar = document.querySelector("[class*='Viewer__sidebar___']");
|
||||
if (!sidebar) return;
|
||||
|
||||
const ol = sidebar.querySelector("ol");
|
||||
if (!ol) return;
|
||||
|
||||
ol.addEventListener("click", (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.closest(".bsplus-folders-section")) return;
|
||||
|
||||
const li = target.closest("li");
|
||||
if (li && ol.contains(li)) {
|
||||
if (activeFolderId !== null) {
|
||||
activeFolderId = null;
|
||||
applyFolderFilter();
|
||||
applyBadges();
|
||||
renderSidebarFolders();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ── "Add to folder" button in message action bar ──
|
||||
|
||||
const injectFolderButton = (actionsBar: Element) => {
|
||||
if (actionsBar.querySelector(".bsplus-folder-btn")) return;
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "bsplus-folder-btn";
|
||||
wrapper.style.position = "relative";
|
||||
wrapper.style.display = "inline-block";
|
||||
|
||||
const btn = document.createElement("button");
|
||||
const btnClasses = actionsBar.querySelector("button")?.className ?? "";
|
||||
btn.className = btnClasses;
|
||||
btn.title = "Add to folder";
|
||||
btn.innerHTML = FOLDER_ICON_SVG;
|
||||
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeDropdown();
|
||||
|
||||
const selectedMsg = document.querySelector("[class*='MessageList__selected___']");
|
||||
const messageId = selectedMsg?.getAttribute("data-message");
|
||||
if (!messageId) return;
|
||||
|
||||
showFolderDropdown(wrapper, messageId);
|
||||
});
|
||||
|
||||
wrapper.appendChild(btn);
|
||||
|
||||
const moreMenu = actionsBar.querySelector("[class*='MenuButton__Menu___']");
|
||||
if (moreMenu) {
|
||||
actionsBar.insertBefore(wrapper, moreMenu);
|
||||
} else {
|
||||
actionsBar.appendChild(wrapper);
|
||||
}
|
||||
};
|
||||
|
||||
const showFolderDropdown = (anchor: HTMLElement, messageId: string) => {
|
||||
const dropdown = document.createElement("div");
|
||||
dropdown.className = "bsplus-folder-dropdown";
|
||||
|
||||
const folders = getFolders();
|
||||
const currentFolderIds = getMessageFolderIds(messageId);
|
||||
|
||||
if (folders.length === 0) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "bsplus-folder-dropdown-empty";
|
||||
empty.textContent = "No folders yet";
|
||||
dropdown.appendChild(empty);
|
||||
} else {
|
||||
for (const folder of folders) {
|
||||
const isChecked = currentFolderIds.includes(folder.id);
|
||||
const item = document.createElement("button");
|
||||
item.className = `bsplus-folder-dropdown-item${isChecked ? " bsplus-checked" : ""}`;
|
||||
|
||||
const check = document.createElement("div");
|
||||
check.className = "bsplus-folder-dropdown-check";
|
||||
check.style.borderColor = isChecked ? folder.color : "";
|
||||
check.style.background = isChecked ? folder.color : "";
|
||||
check.innerHTML = CHECK_SVG_WHITE;
|
||||
|
||||
const dot = document.createElement("div");
|
||||
dot.className = "bsplus-folder-dot";
|
||||
dot.style.background = folder.color;
|
||||
|
||||
const name = document.createElement("span");
|
||||
name.textContent = folder.name;
|
||||
|
||||
item.appendChild(check);
|
||||
item.appendChild(dot);
|
||||
item.appendChild(name);
|
||||
|
||||
item.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
toggleMessageInFolder(messageId, folder.id);
|
||||
|
||||
const nowChecked = getMessageFolderIds(messageId).includes(folder.id);
|
||||
item.classList.toggle("bsplus-checked", nowChecked);
|
||||
check.style.borderColor = nowChecked ? folder.color : "";
|
||||
check.style.background = nowChecked ? folder.color : "";
|
||||
|
||||
applyBadges();
|
||||
applyFolderFilter();
|
||||
renderSidebarFolders();
|
||||
});
|
||||
|
||||
dropdown.appendChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
anchor.appendChild(dropdown);
|
||||
openDropdown = dropdown;
|
||||
|
||||
dropdownCloseHandler = (e: MouseEvent) => {
|
||||
if (!dropdown.contains(e.target as Node) && !anchor.contains(e.target as Node)) {
|
||||
closeDropdown();
|
||||
}
|
||||
};
|
||||
setTimeout(() => {
|
||||
document.addEventListener("click", dropdownCloseHandler!, true);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const closeDropdown = () => {
|
||||
if (openDropdown) {
|
||||
openDropdown.remove();
|
||||
openDropdown = null;
|
||||
}
|
||||
if (dropdownCloseHandler) {
|
||||
document.removeEventListener("click", dropdownCloseHandler, true);
|
||||
dropdownCloseHandler = null;
|
||||
}
|
||||
};
|
||||
|
||||
// ── Message badges ──
|
||||
|
||||
const applyBadges = () => {
|
||||
const messageItems = document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]");
|
||||
|
||||
if (!shouldShowBadgesInList()) {
|
||||
for (const li of messageItems) {
|
||||
const subject = li.querySelector("[class*='MessageList__subject___']");
|
||||
if (subject && (subject.querySelector(".bsplus-msg-badges") || subject.querySelector(".bsplus-subject-text"))) {
|
||||
restoreSubjectPlain(subject);
|
||||
} else {
|
||||
li.querySelector(".bsplus-msg-badges")?.remove();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const folders = getFolders();
|
||||
const assignments = getAssignments();
|
||||
|
||||
for (const li of messageItems) {
|
||||
const msgId = li.getAttribute("data-message");
|
||||
if (!msgId) continue;
|
||||
|
||||
let badgeContainer = li.querySelector(".bsplus-msg-badges") as HTMLElement | null;
|
||||
|
||||
const folderIds = [];
|
||||
for (const [fId, mIds] of Object.entries(assignments)) {
|
||||
if (mIds.includes(msgId)) folderIds.push(fId);
|
||||
}
|
||||
|
||||
if (folderIds.length === 0) {
|
||||
badgeContainer?.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!badgeContainer) {
|
||||
badgeContainer = document.createElement("div");
|
||||
badgeContainer.className = "bsplus-msg-badges";
|
||||
const subject = li.querySelector("[class*='MessageList__subject___']");
|
||||
if (subject) {
|
||||
if (!subject.querySelector(".bsplus-subject-text")) {
|
||||
const textWrap = document.createElement("span");
|
||||
textWrap.className = "bsplus-subject-text";
|
||||
textWrap.textContent = subject.textContent;
|
||||
subject.textContent = "";
|
||||
subject.appendChild(textWrap);
|
||||
}
|
||||
subject.appendChild(badgeContainer);
|
||||
} else {
|
||||
li.appendChild(badgeContainer);
|
||||
}
|
||||
}
|
||||
|
||||
badgeContainer.innerHTML = "";
|
||||
for (const fId of folderIds) {
|
||||
const folder = folders.find((f) => f.id === fId);
|
||||
if (!folder) continue;
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "bsplus-msg-badge";
|
||||
badge.style.background = folder.color;
|
||||
badge.textContent = folder.name;
|
||||
badge.title = `Filter by "${folder.name}"`;
|
||||
badge.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
activeFolderId = folder.id;
|
||||
applyFolderFilter();
|
||||
applyBadges();
|
||||
renderSidebarFolders();
|
||||
});
|
||||
badgeContainer.appendChild(badge);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ── Folder filtering ──
|
||||
|
||||
const applyFolderFilter = () => {
|
||||
const messageItems = document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]");
|
||||
const moreBtn = document.querySelector("[class*='MessageList__MessageList___'] ol > button");
|
||||
|
||||
if (activeFolderId === null) {
|
||||
if (api.settings.hideFolderedMessagesInAll) {
|
||||
for (const li of messageItems) {
|
||||
const msgId = li.getAttribute("data-message");
|
||||
if (msgId && isMessageInAnyCustomFolder(msgId)) {
|
||||
li.classList.add("bsplus-folder-hidden");
|
||||
} else {
|
||||
li.classList.remove("bsplus-folder-hidden");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const li of messageItems) {
|
||||
li.classList.remove("bsplus-folder-hidden");
|
||||
}
|
||||
}
|
||||
if (moreBtn) (moreBtn as HTMLElement).classList.remove("bsplus-folder-hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
const folderMsgIds = getAssignments()[activeFolderId] ?? [];
|
||||
|
||||
for (const li of messageItems) {
|
||||
const msgId = li.getAttribute("data-message");
|
||||
if (msgId && folderMsgIds.includes(msgId)) {
|
||||
li.classList.remove("bsplus-folder-hidden");
|
||||
} else {
|
||||
li.classList.add("bsplus-folder-hidden");
|
||||
}
|
||||
}
|
||||
if (moreBtn) (moreBtn as HTMLElement).classList.add("bsplus-folder-hidden");
|
||||
};
|
||||
|
||||
// ── Observers ──
|
||||
|
||||
const setupMessageListObserver = () => {
|
||||
const messageList = document.querySelector("[class*='MessageList__MessageList___'] ol");
|
||||
if (!messageList || messageListObserver) return;
|
||||
|
||||
messageListObserver = new MutationObserver(() => {
|
||||
applyBadges();
|
||||
applyFolderFilter();
|
||||
});
|
||||
messageListObserver.observe(messageList, { childList: true, subtree: false });
|
||||
};
|
||||
|
||||
const setupActionsObserver = () => {
|
||||
if (actionsObserver) return;
|
||||
|
||||
const target = document.querySelector("[class*='Viewer__Viewer___']") ?? document.querySelector("div.messages");
|
||||
if (!target) return;
|
||||
|
||||
actionsObserver = new MutationObserver(() => {
|
||||
const actionsBar = document.querySelector("[class*='Message__actions___']");
|
||||
if (actionsBar && !actionsBar.querySelector(".bsplus-folder-btn")) {
|
||||
injectFolderButton(actionsBar);
|
||||
}
|
||||
});
|
||||
actionsObserver.observe(target, { childList: true, subtree: true });
|
||||
};
|
||||
|
||||
// ── Main page handler ──
|
||||
|
||||
const handleMessagesPage = async () => {
|
||||
await waitForElm("[class*='Viewer__sidebar___'] ol", true, 50, 100);
|
||||
|
||||
renderSidebarFolders();
|
||||
attachNativeSidebarListeners();
|
||||
|
||||
await waitForElm("[class*='MessageList__MessageList___'] ol", true, 50, 100);
|
||||
applyBadges();
|
||||
applyFolderFilter();
|
||||
setupMessageListObserver();
|
||||
|
||||
// The actions bar only exists when a message is selected/open,
|
||||
// so we observe the whole viewer for it to appear dynamically
|
||||
setupActionsObserver();
|
||||
|
||||
// If a message is already selected, inject immediately
|
||||
const actionsBar = document.querySelector("[class*='Message__actions___']");
|
||||
if (actionsBar) injectFolderButton(actionsBar);
|
||||
|
||||
// Re-observe the sidebar for SEQTA re-renders
|
||||
const sidebar = document.querySelector("[class*='Viewer__sidebar___']");
|
||||
if (sidebar && !sidebarObserver) {
|
||||
sidebarObserver = new MutationObserver(() => {
|
||||
const ol = sidebar.querySelector("ol");
|
||||
if (ol && !ol.querySelector(".bsplus-folders-section")) {
|
||||
renderSidebarFolders();
|
||||
attachNativeSidebarListeners();
|
||||
}
|
||||
});
|
||||
sidebarObserver.observe(sidebar, { childList: true, subtree: true });
|
||||
}
|
||||
};
|
||||
|
||||
// ── Lifecycle ──
|
||||
|
||||
const mountUnsub = api.seqta.onMount("div.messages", handleMessagesPage);
|
||||
unregisters.push(mountUnsub);
|
||||
|
||||
unregisters.push(
|
||||
api.settings.onChange("showTagsInAllMessages", () => {
|
||||
applyBadges();
|
||||
}),
|
||||
);
|
||||
unregisters.push(
|
||||
api.settings.onChange("hideFolderedMessagesInAll", () => {
|
||||
applyFolderFilter();
|
||||
}),
|
||||
);
|
||||
|
||||
return () => {
|
||||
for (const u of unregisters) u.unregister();
|
||||
messageListObserver?.disconnect();
|
||||
sidebarObserver?.disconnect();
|
||||
actionsObserver?.disconnect();
|
||||
closeDropdown();
|
||||
styleEl.remove();
|
||||
document.querySelectorAll(".bsplus-folders-section").forEach((el) => el.remove());
|
||||
document.querySelectorAll(".bsplus-folder-btn").forEach((el) => el.remove());
|
||||
document.querySelectorAll(".bsplus-msg-badges").forEach((el) => el.remove());
|
||||
document.querySelectorAll("[class*='MessageList__subject___']").forEach((subject) => {
|
||||
if (subject.querySelector(".bsplus-subject-text")) {
|
||||
restoreSubjectPlain(subject);
|
||||
}
|
||||
});
|
||||
document.querySelectorAll(".bsplus-folder-hidden").forEach((el) =>
|
||||
el.classList.remove("bsplus-folder-hidden"),
|
||||
);
|
||||
document.querySelectorAll(".bsplus-modal-overlay").forEach((el) => el.remove());
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default messageFoldersPlugin;
|
||||
@@ -0,0 +1,491 @@
|
||||
/* ── Sidebar folder section ── */
|
||||
.bsplus-folders-section {
|
||||
border-top: 1px solid var(--background-secondary, rgba(128, 128, 128, 0.2));
|
||||
margin-top: 4px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.bsplus-folders-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px 2px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.bsplus-folders-header span {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--text-primary, #666);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.bsplus-folders-add-btn {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
min-width: 0 !important;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
border-radius: 4px !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
transition: all 0.2s ease;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.bsplus-folders-add-btn:hover {
|
||||
opacity: 1;
|
||||
background: var(--background-secondary, rgba(128, 128, 128, 0.1)) !important;
|
||||
}
|
||||
|
||||
/* ── Folder list items ── */
|
||||
.bsplus-folder-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
position: relative;
|
||||
gap: 8px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.bsplus-folder-item:hover {
|
||||
background: var(--theme-offset-bg-more, rgba(128, 128, 128, 0.08));
|
||||
}
|
||||
|
||||
.bsplus-folder-item.bsplus-folder-active {
|
||||
background: var(--theme-offset-bg-more, rgba(128, 128, 128, 0.12));
|
||||
}
|
||||
|
||||
.bsplus-folder-item.bsplus-folder-active::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: var(--better-main, #007bff);
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
.bsplus-folder-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bsplus-folder-name {
|
||||
font-size: 13px;
|
||||
color: var(--text-primary, #333);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bsplus-folder-count {
|
||||
font-size: 11px;
|
||||
color: var(--text-primary, #999);
|
||||
opacity: 0.5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bsplus-folder-actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.bsplus-folder-item:hover .bsplus-folder-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.bsplus-folder-action-btn {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
min-width: 0 !important;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
border-radius: 4px !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.bsplus-folder-action-btn:hover {
|
||||
opacity: 1;
|
||||
background: var(--background-secondary, rgba(128, 128, 128, 0.15)) !important;
|
||||
}
|
||||
|
||||
/* ── Inline folder name input ── */
|
||||
.bsplus-folder-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.bsplus-folder-input input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
border: 1px solid var(--background-secondary, #ccc);
|
||||
border-radius: 6px;
|
||||
background: var(--background-secondary, #f5f5f5);
|
||||
color: var(--text-primary, #333);
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.bsplus-folder-input input:focus {
|
||||
border-color: var(--better-main, #007bff);
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.2);
|
||||
}
|
||||
|
||||
.bsplus-folder-input-confirm,
|
||||
.bsplus-folder-input-cancel {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
min-width: 0 !important;
|
||||
border: none !important;
|
||||
border-radius: 4px !important;
|
||||
cursor: pointer;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.bsplus-folder-input-confirm {
|
||||
background: var(--better-main, #007bff) !important;
|
||||
}
|
||||
|
||||
.bsplus-folder-input-confirm:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.bsplus-folder-input-cancel {
|
||||
background: transparent !important;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.bsplus-folder-input-cancel:hover {
|
||||
opacity: 1;
|
||||
background: var(--background-secondary, rgba(128, 128, 128, 0.1)) !important;
|
||||
}
|
||||
|
||||
/* ── Color picker row ── */
|
||||
.bsplus-folder-colors {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 6px;
|
||||
padding: 4px 12px 6px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.bsplus-folder-color-opt {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
padding: 0;
|
||||
background: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bsplus-folder-color-opt:hover {
|
||||
transform: scale(1.25);
|
||||
box-shadow: 0 0 0 3px rgba(128, 128, 128, 0.15);
|
||||
}
|
||||
|
||||
.bsplus-folder-color-opt.bsplus-color-selected {
|
||||
border-color: var(--text-primary, #333);
|
||||
transform: scale(1.15);
|
||||
box-shadow: 0 0 0 3px rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
.bsplus-folder-color-opt.bsplus-color-selected:hover {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
|
||||
/* ── "Add to folder" button in message actions bar ── */
|
||||
.bsplus-folder-btn {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bsplus-folder-btn svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* ── Folder dropdown ── */
|
||||
.bsplus-folder-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 4px;
|
||||
min-width: 180px;
|
||||
background: var(--background-primary, #fff);
|
||||
border: 1px solid var(--background-secondary, #e0e0e0);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
animation: bsplus-dropdown-in 0.15s ease-out;
|
||||
}
|
||||
|
||||
@keyframes bsplus-dropdown-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px) scale(0.97);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.bsplus-folder-dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s ease;
|
||||
border: none;
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
color: var(--text-primary, #333);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.bsplus-folder-dropdown-item:hover {
|
||||
background: var(--theme-offset-bg-more, rgba(128, 128, 128, 0.08));
|
||||
}
|
||||
|
||||
.bsplus-folder-dropdown-check {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid var(--background-secondary, #ccc);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.bsplus-folder-dropdown-item.bsplus-checked .bsplus-folder-dropdown-check {
|
||||
background: var(--better-main, #007bff);
|
||||
border-color: var(--better-main, #007bff);
|
||||
}
|
||||
|
||||
.bsplus-folder-dropdown-check svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
color: white;
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s ease;
|
||||
}
|
||||
|
||||
.bsplus-folder-dropdown-item.bsplus-checked .bsplus-folder-dropdown-check svg {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.bsplus-folder-dropdown-empty {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--text-primary, #999);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* ── Let primary column use available space instead of being clipped ── */
|
||||
[class*='MessageList__primary___'] {
|
||||
flex: 1 1 0% !important;
|
||||
min-width: 0 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* ── Make subject line a flex row so badges sit inline ── */
|
||||
[class*='MessageList__subject___'] {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* ── Subject text truncates to make room for badges ── */
|
||||
.bsplus-subject-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
/* ── Shrink the secondary column to its content ── */
|
||||
[class*='MessageList__secondary___'] {
|
||||
flex: 0 0 auto !important;
|
||||
width: auto !important;
|
||||
min-width: 0 !important;
|
||||
max-width: 200px !important;
|
||||
}
|
||||
|
||||
/* ── Constrain the flags/attachment icon column ── */
|
||||
[class*='MessageList__flags___'] {
|
||||
width: 24px !important;
|
||||
min-width: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
/* ── Message list folder badges ── */
|
||||
.bsplus-msg-badges {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.bsplus-msg-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.bsplus-msg-badge:hover {
|
||||
opacity: 0.85;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* ── Folder filtering (hide messages not in active folder) ── */
|
||||
.bsplus-folder-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ── Delete confirmation modal ── */
|
||||
@keyframes bsplus-modal-overlay-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes bsplus-modal-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.bsplus-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2147483647;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
animation: bsplus-modal-overlay-in 0.2s ease-out forwards;
|
||||
}
|
||||
|
||||
.bsplus-modal {
|
||||
padding: 1rem 1.5rem;
|
||||
margin: 0 1rem;
|
||||
min-width: 16rem;
|
||||
max-width: 22rem;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: var(--background-primary, #fff);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid var(--background-secondary, #e0e0e0);
|
||||
animation: bsplus-modal-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||||
}
|
||||
|
||||
.bsplus-modal h3 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #333);
|
||||
}
|
||||
|
||||
.bsplus-modal p {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary, #666);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.bsplus-modal-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.bsplus-modal-actions button {
|
||||
padding: 0.4rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.bsplus-modal-btn-cancel {
|
||||
background: transparent;
|
||||
border: 1px solid var(--background-secondary, #ccc);
|
||||
color: var(--text-primary, #333);
|
||||
}
|
||||
|
||||
.bsplus-modal-btn-cancel:hover {
|
||||
background: var(--background-secondary, rgba(128, 128, 128, 0.1));
|
||||
}
|
||||
|
||||
.bsplus-modal-btn-danger {
|
||||
background: #e53e3e;
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.bsplus-modal-btn-danger:hover {
|
||||
background: #c53030;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(229, 62, 62, 0.35);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import assessmentsAveragePlugin from "./built-in/assessmentsAverage";
|
||||
import profilePicturePlugin from "./built-in/profilePicture";
|
||||
import assessmentsOverviewPlugin from "./built-in/assessmentsOverview";
|
||||
import backgroundMusicPlugin from "./built-in/backgroundMusic";
|
||||
import messageFoldersPlugin from "./built-in/messageFolders";
|
||||
//import testPlugin from './built-in/test';
|
||||
|
||||
// Heavy plugins (lazy-loaded only when enabled)
|
||||
@@ -28,6 +29,7 @@ pluginManager.registerPlugin(timetableEditPlugin);
|
||||
pluginManager.registerPlugin(profilePicturePlugin);
|
||||
pluginManager.registerPlugin(assessmentsOverviewPlugin);
|
||||
pluginManager.registerPlugin(backgroundMusicPlugin);
|
||||
pluginManager.registerPlugin(messageFoldersPlugin);
|
||||
//pluginManager.registerPlugin(testPlugin);
|
||||
|
||||
// Register heavy plugins with lazy loading
|
||||
|
||||
Reference in New Issue
Block a user