* HeaderBarApi: fix minor style issue

* HolyNotes: replace icon

* Components.dev: remove popout

* HeaderBarApi: fix tooltip

* HolyNotes: replace icon again

* HolyNotes: refactor

* Switch: fix thumb color for whtie theme

* BetterUserArea: fix patch

* ModalCloseButton: remove dups and export properly

* bleh

* Update NotebookCreateModal.tsx

* bleh

---------

Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
prism 2026-01-30 14:10:22 +01:00 • committed by GitHub
parent 2ae1062a28
commit 7d532a3bfb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 697 additions and 1500 deletions

View file

@ -98,12 +98,15 @@ export function HeaderBarButton(props: HeaderBarButtonProps & { ref?: React.RefO
return (
<Tooltip text={tooltip ?? ""} position={position} shouldShow={tooltip != null}>
{() => (
{({ onMouseEnter, onMouseLeave }) => (
<Clickable
{...{ innerRef: ref } as any}
className={classes(HeaderBarClasses.clickable, HeaderBarClasses.withHighlight, className)}
style={{ width: iconSize, boxSizing: "content-box", justifyContent: "center" }}
onClick={onClick}
onContextMenu={onContextMenu}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
role="button"
tabIndex={0}
aria-label={label}

View file

@ -61,6 +61,10 @@
left: 24px;
}
.vc-switch-checked .vc-switch-thumb rect {
fill: var(--switch-thumb-background-selected-default);
}
.vc-switch-input {
border-radius: 14px;
cursor: pointer;

View file

@ -103,8 +103,8 @@ export default definePlugin({
replace: "tooltipText:void 0}"
},
{
match: /(?<=useRef\(null\);)(\i)=.{0,100}#{intl::USER_SETTINGS}\);/,
replace: "$1=void 0;"
match: /tooltipText:\i,tooltipPositionKey/,
replace: "tooltipText:void 0,tooltipPositionKey"
}
],
predicate: () => settings.store.removeButtonTooltips

View file

@ -40,7 +40,6 @@ import MenuTab from "./tabs/MenuTab";
import ModalTab from "./tabs/ModalTab";
import NoticeTab from "./tabs/NoticeTab";
import PaginatorTab from "./tabs/PaginatorTab";
import PopoutTab from "./tabs/PopoutTab";
import PopoverTab from "./tabs/PopoverTab";
import ProgressBarTab from "./tabs/ProgressBarTab";
import RadioGroupTab from "./tabs/RadioGroupTab";
@ -90,7 +89,6 @@ const TABS = [
{ id: "modal", label: "Modal" },
{ id: "notice", label: "Notice" },
{ id: "paginator", label: "Paginator" },
{ id: "popout", label: "Popout" },
{ id: "popover", label: "Popover" },
{ id: "progressbar", label: "ProgressBar" },
{ id: "radiogroup", label: "RadioGroup" },
@ -144,7 +142,6 @@ const TAB_COMPONENTS: Record<TabId, React.ComponentType> = {
modal: ModalTab,
notice: NoticeTab,
paginator: PaginatorTab,
popout: PopoutTab,
popover: PopoverTab,
progressbar: ProgressBarTab,
radiogroup: RadioGroupTab,

View file

@ -75,7 +75,6 @@ import type {
NumberBadgeProps,
OpenModalOptions,
PaginatorProps,
PopoutComponent,
ProgressBarProps,
SearchBarProps,
SimpleErrorBoundaryProps,
@ -150,7 +149,6 @@ export const Accordion = findComponentByCodeLazy("scrollHeight)}", "onExpandedCh
export const Timestamp = findComponentByCodeLazy("#{intl::MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL}", "isVisibleOnlyOnHover") as React.ComponentType<TimestampProps>;
export const GuildIcon = findComponentByCodeLazy("Masks.CLAN_ICON,width", "acronymClassName") as React.ComponentType<GuildIconProps>;
export const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", "showEyeDropper") as React.ComponentType<ColorPickerProps>;
export const Popout: PopoutComponent = proxyLazy(() => findLazy(m => m?.y?.Animation && m.y.toString?.().includes("renderPopout")).y);
export const FocusLock = findComponentByCodeLazy(".containerRef,{keyboardModeEnabled:") as React.ComponentType<FocusLockProps>;
export const UserSummaryItem = findComponentByCodeLazy("popoutUserId") as React.ComponentType<UserSummaryItemProps>;

View file

@ -1,124 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Clickable, ManaButton, Paragraph, Popout, useState } from "..";
import { SectionWrapper } from "../SectionWrapper";
export default function PopoutTab() {
const [showBasic, setShowBasic] = useState(false);
const [showPositions, setShowPositions] = useState<Record<string, boolean>>({});
const positions = ["top", "bottom", "left", "right"] as const;
return (
<div className="vc-compfinder-section">
<SectionWrapper title="Basic Popout">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Click the button to toggle a popout.
</Paragraph>
<Popout
shouldShow={showBasic}
onRequestClose={() => setShowBasic(false)}
renderPopout={() => (
<div style={{ padding: 16, background: "var(--background-floating)", borderRadius: 8 }}>
<Paragraph>This is a popout!</Paragraph>
<ManaButton
variant="secondary"
text="Close"
onClick={() => setShowBasic(false)}
style={{ marginTop: 8 }}
/>
</div>
)}
>
{(props, { isShown }) => (
<Clickable {...props}>
<ManaButton
variant={isShown ? "primary" : "secondary"}
text={isShown ? "Popout Open" : "Open Popout"}
onClick={() => setShowBasic(!showBasic)}
/>
</Clickable>
)}
</Popout>
</SectionWrapper>
<SectionWrapper title="Positions">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Popout can appear in different positions.
</Paragraph>
<div className="vc-compfinder-grid">
{positions.map(position => (
<Popout
key={position}
position={position}
shouldShow={showPositions[position] ?? false}
onRequestClose={() => setShowPositions(p => ({ ...p, [position]: false }))}
renderPopout={() => (
<div style={{ padding: 12, background: "var(--background-floating)", borderRadius: 8 }}>
<Paragraph>Position: {position}</Paragraph>
</div>
)}
>
{(props, { isShown }) => (
<Clickable {...props}>
<ManaButton
variant={isShown ? "primary" : "secondary"}
text={position}
onClick={() => setShowPositions(p => ({ ...p, [position]: !p[position] }))}
/>
</Clickable>
)}
</Popout>
))}
</div>
</SectionWrapper>
<SectionWrapper title="Animation Types">
<Paragraph color="text-muted" style={{ marginBottom: 8 }}>
Popout supports different animation types via Popout.Animation.
</Paragraph>
<div className="vc-compfinder-grid">
{(["NONE", "TRANSLATE", "SCALE", "FADE"] as const).map(anim => (
<Popout
key={anim}
animation={Popout.Animation[anim]}
shouldShow={showPositions[`anim-${anim}`] ?? false}
onRequestClose={() => setShowPositions(p => ({ ...p, [`anim-${anim}`]: false }))}
renderPopout={() => (
<div style={{ padding: 12, background: "var(--background-floating)", borderRadius: 8 }}>
<Paragraph>Animation: {anim}</Paragraph>
</div>
)}
>
{(props, { isShown }) => (
<Clickable {...props}>
<ManaButton
variant={isShown ? "primary" : "secondary"}
text={anim}
onClick={() => setShowPositions(p => ({ ...p, [`anim-${anim}`]: !p[`anim-${anim}`] }))}
/>
</Clickable>
)}
</Popout>
))}
</div>
</SectionWrapper>
<SectionWrapper title="Props">
<Paragraph color="text-muted">• children - Render function (props, data) =&gt; ReactNode</Paragraph>
<Paragraph color="text-muted">• renderPopout - Render function for popout content</Paragraph>
<Paragraph color="text-muted">• shouldShow - Control visibility</Paragraph>
<Paragraph color="text-muted">• position - top, bottom, left, right, center, window_center</Paragraph>
<Paragraph color="text-muted">• align - left, right, center, top, bottom</Paragraph>
<Paragraph color="text-muted">• animation - Popout.Animation.NONE/TRANSLATE/SCALE/FADE</Paragraph>
<Paragraph color="text-muted">• spacing - Distance from target (default 8)</Paragraph>
<Paragraph color="text-muted">• onRequestOpen/onRequestClose - Callbacks</Paragraph>
<Paragraph color="text-muted">• autoInvert - Auto-flip if near edge (default true)</Paragraph>
</SectionWrapper>
</div>
);
}

View file

@ -25,7 +25,6 @@ export * from "./menu";
export * from "./modal";
export * from "./notice";
export * from "./paginator";
export * from "./popout";
export * from "./progress";
export * from "./radio";
export * from "./scroller";

View file

@ -1,72 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export type PopoutAnimationType = "1" | "2" | "3" | "4";
export type PopoutPosition = "top" | "bottom" | "left" | "right" | "center" | "window_center";
export type PopoutAlign = "left" | "right" | "center" | "top" | "bottom";
export interface PopoutChildrenProps {
"aria-controls": string;
"aria-expanded": boolean;
onClick: (event: React.MouseEvent<HTMLElement>) => void;
onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => void;
onMouseDown: (event: React.MouseEvent<HTMLElement>) => void;
}
export interface PopoutChildrenData {
isShown: boolean;
position: PopoutPosition;
}
export interface PopoutRenderProps {
closePopout: () => void;
isPositioned: boolean;
nudge: number;
position: PopoutPosition;
setPopoutRef: (ref: any) => void;
updatePosition: () => void;
}
export interface PopoutProps {
children: (props: PopoutChildrenProps, data: PopoutChildrenData) => React.ReactNode;
renderPopout: (props: PopoutRenderProps) => React.ReactNode;
shouldShow?: boolean;
targetElementRef?: React.RefObject<HTMLElement>;
onRequestOpen?: () => void;
onRequestClose?: () => void;
align?: PopoutAlign;
animation?: PopoutAnimationType;
animationPosition?: PopoutPosition;
autoInvert?: boolean;
nudgeAlignIntoViewport?: boolean;
position?: PopoutPosition;
positionKey?: string;
popoutKey?: string;
spacing?: number;
preload?: boolean;
fixed?: boolean;
useRawTargetDimensions?: boolean;
onShiftClick?: () => void;
disablePointerEvents?: boolean;
ignoreModalClicks?: boolean;
scrollBehavior?: string;
useMouseEnter?: boolean;
layerContext?: any;
clickTrap?: boolean;
}
export interface PopoutAnimationEnum {
NONE: "1";
TRANSLATE: "2";
SCALE: "3";
FADE: "4";
}
export type PopoutComponent = React.ComponentType<PopoutProps> & {
Animation: PopoutAnimationEnum;
};

View file

@ -4,209 +4,146 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { makeDummyUser } from "@components/settings/tabs/plugins/PluginModal";
import { Channel, Message } from "@vencord/discord-types";
import { proxyLazyWebpack } from "@webpack";
import { ChannelStore, lodash, Toasts, UserStore } from "@webpack/common";
import { ChannelStore, Toasts, UserStore } from "@webpack/common";
import { Discord, HolyNotes } from "./types";
import { AllNotebooks, Note, Notebook } from "./types";
import { deleteCacheFromDataStore, DeleteEntireStore, saveCacheToDataStore } from "./utils";
export const noteHandlerCache = new Map();
export const noteHandlerCache = new Map<string, Notebook>();
export const noteHandler = proxyLazyWebpack(() => {
class NoteHandler {
public _formatNote(channel: Channel, message: Message): HolyNotes.Note {
return {
id: message.id,
channel_id: message.channel_id,
guild_id: channel.guild_id,
content: message.content,
author: {
id: message.author.id,
avatar: message.author.avatar,
discriminator: message.author.discriminator,
username: message.author.username,
},
flags: message.flags,
// Moment has a toString() function, this doesn't convert to '[object Object]'.
function showToast(message: string, type: typeof Toasts.Type[keyof typeof Toasts.Type] = Toasts.Type.SUCCESS) {
Toasts.show({ id: Toasts.genId(), message, type });
}
timestamp: message.timestamp.toString(),
attachments: message.attachments as Discord.Attachment[],
embeds: message.embeds,
reactions: message.reactions as Discord.Reaction[],
stickerItems: message.stickerItems,
};
function formatNote(channel: Channel, message: Message): Note {
return {
id: message.id,
channel_id: message.channel_id,
guild_id: channel.guild_id,
content: message.content,
author: {
id: message.author.id,
avatar: message.author.avatar,
discriminator: message.author.discriminator,
username: message.author.username,
},
flags: message.flags,
timestamp: message.timestamp.toString(),
attachments: message.attachments,
embeds: message.embeds,
reactions: message.reactions,
stickerItems: message.stickerItems,
};
}
export const noteHandler = proxyLazyWebpack(() => ({
getNotes(notebook: string): Notebook | undefined {
return noteHandlerCache.get(notebook);
},
getAllNotes(): AllNotebooks {
return Object.fromEntries(noteHandlerCache.entries());
},
addNote(message: Message, notebook: string) {
const notes = this.getNotes(notebook) ?? {};
const channel = ChannelStore.getChannel(message.channel_id);
const newNotes = { [message.id]: formatNote(channel, message), ...notes };
noteHandlerCache.set(notebook, newNotes);
saveCacheToDataStore(notebook, newNotes);
showToast(`Added note to ${notebook}`);
},
deleteNote(noteId: string, notebook: string) {
const notes = this.getNotes(notebook);
if (!notes) return;
const { [noteId]: _, ...rest } = notes;
noteHandlerCache.set(notebook, rest);
saveCacheToDataStore(notebook, rest);
showToast(`Deleted note from ${notebook}`);
},
moveNote(note: Note, from: string, to: string) {
const fromNotes = this.getNotes(from);
const toNotes = this.getNotes(to);
if (!fromNotes || !toNotes) return;
const { [note.id]: _, ...restFrom } = fromNotes;
const newTo = { [note.id]: note, ...toNotes };
noteHandlerCache.set(from, restFrom);
noteHandlerCache.set(to, newTo);
saveCacheToDataStore(from, restFrom);
saveCacheToDataStore(to, newTo);
showToast(`Moved note from ${from} to ${to}`);
},
newNoteBook(name: string, silent?: boolean) {
if (noteHandlerCache.has(name)) {
showToast(`Notebook ${name} already exists`, Toasts.Type.FAILURE);
return;
}
public getNotes(notebook?: string): Record<string, HolyNotes.Note> {
return noteHandlerCache.get(notebook);
}
noteHandlerCache.set(name, {});
saveCacheToDataStore(name, {});
if (!silent) showToast(`Created notebook ${name}`);
},
public getAllNotes(): HolyNotes.Note[] {
const data = noteHandlerCache.entries();
const notes = {};
for (const [key, value] of data) {
notes[key] = value;
}
return notes as HolyNotes.Note[];
}
deleteNotebook(name: string) {
noteHandlerCache.delete(name);
deleteCacheFromDataStore(name);
showToast(`Deleted notebook ${name}`);
},
public addNote = async (message: Message, notebook: string) => {
const notes = this.getNotes(notebook);
const channel = ChannelStore.getChannel(message.channel_id);
const newNotes = Object.assign({ [message.id]: this._formatNote(channel, message) }, notes);
refreshAvatars() {
const notebooks = this.getAllNotes();
noteHandlerCache.set(notebook, newNotes);
saveCacheToDataStore(notebook, newNotes as unknown as HolyNotes.Note[]);
Toasts.show({
id: Toasts.genId(),
message: `Successfully added note to ${notebook}.`,
type: Toasts.Type.SUCCESS,
});
};
public deleteNote = async (noteId: string, notebook: string) => {
const notes = this.getNotes(notebook);
noteHandlerCache.set(notebook, lodash.omit(notes, noteId));
saveCacheToDataStore(notebook, lodash.omit(notes, noteId) as unknown as HolyNotes.Note[]);
Toasts.show({
id: Toasts.genId(),
message: `Successfully deleted note from ${notebook}.`,
type: Toasts.Type.SUCCESS,
});
};
public moveNote = async (note: HolyNotes.Note, from: string, to: string) => {
const origNotebook = this.getNotes(from);
const newNoteBook = lodash.cloneDeep(this.getNotes(to));
newNoteBook[note.id] = note;
noteHandlerCache.set(from, lodash.omit(origNotebook, note.id));
noteHandlerCache.set(to, newNoteBook);
saveCacheToDataStore(from, lodash.omit(origNotebook, note.id) as unknown as HolyNotes.Note[]);
saveCacheToDataStore(to, newNoteBook as unknown as HolyNotes.Note[]);
Toasts.show({
id: Toasts.genId(),
message: `Successfully moved note from ${from} to ${to}.`,
type: Toasts.Type.SUCCESS,
});
};
public newNoteBook = async (notebookName: string, silent?: Boolean) => {
let notebookExists = false;
for (const key of noteHandlerCache.keys()) {
if (key === notebookName) {
notebookExists = true;
break;
for (const notebookName in notebooks) {
const notebook = notebooks[notebookName];
for (const noteId in notebook) {
const note = notebook[noteId];
const user = UserStore.getUser(note.author.id);
if (user) {
note.author.avatar = user.avatar;
note.author.discriminator = user.discriminator;
note.author.username = user.username;
}
}
noteHandlerCache.set(notebookName, notebook);
saveCacheToDataStore(notebookName, notebook);
}
if (notebookExists) {
Toasts.show({
id: Toasts.genId(),
message: `Notebook ${notebookName} already exists.`,
type: Toasts.Type.FAILURE,
});
return;
}
showToast("Refreshed avatars");
},
noteHandlerCache.set(notebookName, {});
saveCacheToDataStore(notebookName, {} as HolyNotes.Note[]);
async deleteEverything() {
noteHandlerCache.clear();
await DeleteEntireStore();
showToast("Deleted all notes");
},
if (!silent) return Toasts.show({
id: Toasts.genId(),
message: `Successfully created ${notebookName}.`,
type: Toasts.Type.SUCCESS,
});
};
exportNotes(): AllNotebooks {
return this.getAllNotes();
},
public deleteNotebook = async (notebookName: string) => {
noteHandlerCache.delete(notebookName);
deleteCacheFromDataStore(notebookName);
importNotes(jsonString: string) {
let parsed: AllNotebooks;
try {
parsed = JSON.parse(jsonString);
} catch {
showToast("Invalid JSON", Toasts.Type.FAILURE);
return;
}
Toasts.show({
id: Toasts.genId(),
message: `Successfully deleted ${notebookName}.`,
type: Toasts.Type.SUCCESS,
});
};
for (const notebook in parsed) {
noteHandlerCache.set(notebook, parsed[notebook]);
saveCacheToDataStore(notebook, parsed[notebook]);
}
public refreshAvatars = async () => {
const notebooks = this.getAllNotes();
for (const notebook in notebooks)
for (const noteId in notebooks[notebook]) {
const note = notebooks[notebook][noteId];
const user = UserStore.getUser(note.author.id) ?? makeDummyUser(note.author);
Object.assign(notebooks[notebook][noteId].author, {
avatar: user.avatar,
discriminator: user.discriminator,
username: user.username,
});
}
for (const notebook in notebooks) {
noteHandlerCache.set(notebook, notebooks[notebook]);
saveCacheToDataStore(notebook, notebooks[notebook] as unknown as HolyNotes.Note[]);
}
Toasts.show({
id: Toasts.genId(),
message: "Successfully refreshed avatars.",
type: Toasts.Type.SUCCESS,
});
};
public deleteEverything = async () => {
noteHandlerCache.clear();
await DeleteEntireStore();
Toasts.show({
id: Toasts.genId(),
message: "Successfully deleted all notes.",
type: Toasts.Type.SUCCESS,
});
};
public exportNotes = async () => {
return this.getAllNotes();
};
public importNotes = async (notes: HolyNotes.Note[]) => {
try {
var parseNotes = JSON.parse(notes as unknown as string);
} catch (e) {
console.log(e);
return Toasts.show({
id: Toasts.genId(),
message: "Invalid JSON.",
type: Toasts.Type.FAILURE,
});
}
for (const notebook in parseNotes) {
noteHandlerCache.set(notebook, parseNotes[notebook]);
saveCacheToDataStore(notebook, parseNotes[notebook] as unknown as HolyNotes.Note[]);
}
Toasts.show({
id: Toasts.genId(),
message: "Successfully imported notes.",
type: Toasts.Type.SUCCESS,
});
};
}
return new NoteHandler();
});
showToast("Imported notes");
},
}));

View file

@ -1,24 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classes } from "@utils/misc";
import { JSX } from "react";
export default ({ className }: { className?: string; }): JSX.Element => (
<svg
x="0"
y="0"
className={classes("vc-holynotes-icon")}
aria-hidden="true"
role="img"
width="24"
height="24"
viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12 2C6.486 2 2 6.487 2 12C2 17.515 6.486 22 12 22C17.514 22 22 17.515 22 12C22 6.487 17.514 2 12 2ZM12 18.25C11.31 18.25 10.75 17.691 10.75 17C10.75 16.31 11.31 15.75 12 15.75C12.69 15.75 13.25 16.31 13.25 17C13.25 17.691 12.69 18.25 12 18.25ZM13 13.875V15H11V12H12C13.104 12 14 11.103 14 10C14 8.896 13.104 8 12 8C10.896 8 10 8.896 10 10H8C8 7.795 9.795 6 12 6C14.205 6 16 7.795 16 10C16 11.861 14.723 13.429 13 13.875Z"></path>
</svg>
);

View file

@ -1,24 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { JSX } from "react";
const component = (props: React.SVGProps<SVGSVGElement>): JSX.Element => (
<svg
viewBox="6 3.7 16 16"
width={24}
height={24}
{...(props as React.SVGProps<SVGSVGElement>)}
>
<path
fill="currentColor"
d="M15.44,5H12.88v6.42L11.12,9.67,9.38,11.42V5H7a.58.58,0,0,0-.58.58V16.5a.56.56,0,0,0,.09.31l1.18,1.91a.55.55,0,0,0,.49.28H17a.58.58,0,0,0,.58-.58V8.48a.52.52,0,0,0-.07-.27L16,5.31A.6.6,0,0,0,15.44,5Zm.94,12.83H8.52l-.71-1.16h7.4a.58.58,0,0,0,.58-.59V7.41l.59,1.25Z"
/>
</svg>
);
export default component;
export const Popover = component as unknown as (props: unknown) => JSX.Element;

View file

@ -1,33 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { JSX } from "react";
const OverFlowIcon = ({ width = 24, height = 24, color = "var(--interactive-icon-default)", className, ...rest }) => (
<svg
{...rest}
width={width}
height={height}
viewBox="0 0 16 16"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.45329 8.53891L3.26217 13.7844C2.95995 14.0719 2.49772 14.0719 2.21328 13.7844C1.92883 13.497 1.92883 13.0299 2.21328 12.7245L6.88884 7.99999L2.21328 3.27543C1.92883 2.988 1.92883 2.50297 2.21328 2.21555C2.49772 1.92812 2.95995 1.92812 3.26217 2.21555L8.45329 7.47903C8.73774 7.76645 8.73774 8.23352 8.45329 8.53891Z"
fill={color}
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.4533 8.53891L9.26217 13.7844C8.95995 14.0719 8.49772 14.0719 8.21328 13.7844C7.92883 13.497 7.92883 13.0299 8.21328 12.7245L12.8888 7.99999L8.21328 3.27543C7.92883 2.988 7.92883 2.50297 8.21328 2.21555C8.49772 1.92812 8.95995 1.92812 9.26217 2.21555L14.4533 7.47903C14.7377 7.76645 14.7377 8.23352 14.4533 8.53891Z"
fill={color}
/>
</svg>
);
export default OverFlowIcon;
export const SvgOverFlowIcon = OverFlowIcon as unknown as (props: React.SVGProps<SVGSVGElement>) => JSX.Element;

View file

@ -6,43 +6,33 @@
import { resultsClasses } from "@equicordplugins/holyNotes";
export default ({ error }: { error?: Error; } = {}) => {
export default function EmptyNotebook({ error }: { error?: Error; } = {}) {
if (error) {
// Error
console.log(error);
console.error("[HolyNotes] Error:", error);
return (
<div className={resultsClasses.emptyResultsWrap}>
<div className={resultsClasses.emptyResultsContent} style={{ paddingBottom: "0px" }}>
<div className={resultsClasses.emptyResultsContent} style={{ paddingBottom: 0 }}>
<div className={resultsClasses.errorImage} />
<div className={resultsClasses.emptyResultsText}>
There was an error parsing your notes! The issue was logged in your console, press CTRL
+ I to access it! Please visit the support server if you need extra help!
There was an error parsing your notes. Check the console for details.
</div>
</div>
</div>
);
} else if (Math.floor(Math.random() * 100) <= 10)
// Easter egg
return (
<div className={resultsClasses.emptyResultsWrap}>
<div className={resultsClasses.emptyResultsContent} style={{ paddingBottom: "0px" }}>
<div className={`${resultsClasses.noResultsImage} ${resultsClasses.alt}`} />
<div className={resultsClasses.emptyResultsText}>
No notes were found. Empathy banana is here for you.
</div>
}
const isEasterEgg = Math.random() < 0.1;
return (
<div className={resultsClasses.emptyResultsWrap}>
<div className={resultsClasses.emptyResultsContent} style={{ paddingBottom: 0 }}>
<div className={`${resultsClasses.noResultsImage}${isEasterEgg ? ` ${resultsClasses.alt}` : ""}`} />
<div className={resultsClasses.emptyResultsText}>
{isEasterEgg
? "No notes were found. Empathy banana is here for you."
: "No notes were found in this notebook."}
</div>
</div>
);
// Empty notebook
else
return (
<div className={resultsClasses.emptyResultsWrap}>
<div className={resultsClasses.emptyResultsContent} style={{ paddingBottom: "0px" }}>
<div className={resultsClasses.noResultsImage} />
<div className={resultsClasses.emptyResultsText}>
No notes were found saved in this notebook.
</div>
</div>
</div>
);
};
</div>
);
}

View file

@ -5,79 +5,71 @@
*/
import { BaseText } from "@components/BaseText";
import { Paragraph } from "@components/Paragraph";
import { Button } from "@components/Button";
import { Flex } from "@components/Flex";
import { cl } from "@equicordplugins/holyNotes";
import { noteHandler } from "@equicordplugins/holyNotes/NoteHandler";
import { downloadNotes, uploadNotes } from "@equicordplugins/holyNotes/utils";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { Button } from "@webpack/common";
import { CloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
export default ({ onClose, ...modalProps }: ModalProps & { onClose: () => void; }) => {
function HelpSection({ title, children }: { title: string; children: React.ReactNode; }) {
return (
<ModalRoot {...modalProps} className="vc-help-modal" size={ModalSize.MEDIUM}>
<ModalHeader className="vc-help-modal-header">
<BaseText tag="h3" style={{ flexGrow: 1 }}>Help Modal</BaseText>
<ModalCloseButton onClick={onClose} />
<>
<BaseText size="md" weight="semibold" color="text-strong">{title}</BaseText>
<BaseText size="sm" color="text-default" style={{ marginTop: 4 }}>{children}</BaseText>
<hr />
</>
);
}
export default function HelpModal({ onClose, transitionState }: ModalProps) {
return (
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
<ModalHeader separator={false} className={cl("header")}>
<div className={cl("header-content")}>
<BaseText tag="h2" size="lg" weight="semibold" className={cl("title")}>
Help
</BaseText>
<BaseText size="sm" className={cl("description")}>
Learn how to use Holy Notes
</BaseText>
</div>
<div className={cl("header-trailing")}>
<CloseButton onClick={onClose} />
</div>
</ModalHeader>
<ModalContent>
<ModalContent className={cl("content")}>
<div className="vc-help-markdown">
<BaseText size="md">Adding Notes</BaseText>
<Paragraph>
To add a note right click on a message then hover over the "Note Message" item and click the button with the notebook name you would like to note the message to.
</Paragraph>
<div style={{ marginTop: "12px" }}>
<span className="vc-help-protip">Protip:</span>
</div>
<Paragraph style={{ marginTop: "8px" }}>
Clicking the "Note Message" button by itself will note to Main by default!
</Paragraph>
<hr />
<BaseText size="md">Deleting Notes</BaseText>
<Paragraph>
Note you can either right click the note and hit "Delete Note" or you can hold the
'DELETE' key on your keyboard and click on a note; it's like magic!
</Paragraph>
<hr />
<BaseText size="md">Moving Notes</BaseText>
<Paragraph>
To move a note right click on a note and hover over the "Move Note" item and click on
the button corresponding to the notebook you would like to move the note to.
</Paragraph>
<hr />
<BaseText size="md">Jump To Message</BaseText>
<Paragraph>
To jump to the location that the note was originally located at just right click on the
note and hit "Jump to Message".
</Paragraph>
<HelpSection title="Adding Notes">
Right-click a message, hover over "Note Message", and select the notebook.
</HelpSection>
<HelpSection title="Deleting Notes">
Right-click a note and select "Delete Note", or hold DELETE and click a note.
</HelpSection>
<HelpSection title="Moving Notes">
Right-click a note, hover over "Move Note", and select the destination notebook.
</HelpSection>
<HelpSection title="Jump To Message">
Right-click a note and select "Jump to Message" to go to the original message.
</HelpSection>
</div>
</ModalContent>
<ModalFooter>
<div className="vc-help-modal-footer">
<Button
look={Button.Looks.FILLED}
color={Button.Colors.GREEN}
onClick={() => {
noteHandler.refreshAvatars();
}}>Refresh Avatars</Button>
<Button
look={Button.Looks.FILLED}
color={Button.Colors.GREEN}
onClick={() => {
uploadNotes();
}}>Import Notes</Button>
<Button
look={Button.Looks.FILLED}
color={Button.Colors.GREEN}
onClick={() => {
downloadNotes();
}}>Export Notes</Button>
<Button
look={Button.Looks.FILLED}
color={Button.Colors.RED}
onClick={() => {
noteHandler.deleteEverything();
}}>Delete All Notes</Button>
</div>
<Flex style={{ gap: 8, flexWrap: "wrap" }}>
<Button size="small" variant="primary" onClick={() => noteHandler.refreshAvatars()}>
Refresh Avatars
</Button>
<Button size="small" variant="secondary" onClick={uploadNotes}>
Import Notes
</Button>
<Button size="small" variant="secondary" onClick={downloadNotes}>
Export Notes
</Button>
<Button size="small" variant="dangerSecondary" onClick={() => noteHandler.deleteEverything()}>
Delete All Notes
</Button>
</Flex>
</ModalFooter>
</ModalRoot>
);
};
}

View file

@ -1,28 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { openModal } from "@utils/modal";
import { Button, React } from "@webpack/common";
import NotebookCreateModal from "./NotebookCreateModal";
import NotebookDeleteModal from "./NotebookDeleteModal";
export default ({ notebook, setNotebook }: { notebook: string, setNotebook: React.Dispatch<React.SetStateAction<string>>; }) => {
const isNotMain = notebook !== "Main";
return (
<Button
color={isNotMain ? Button.Colors.RED : Button.Colors.GREEN}
onClick={
isNotMain
? () => openModal(props => <NotebookDeleteModal {...props} notebook={notebook} onChangeTab={setNotebook} />)
: () => openModal(props => <NotebookCreateModal {...props} />)
}
>
{isNotMain ? "Delete Notebook" : "Create Notebook"}
</Button>
);
};

View file

@ -1,187 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { SvgOverFlowIcon } from "@equicordplugins/holyNotes/components/icons/overFlowIcon";
import { classes } from "@utils/misc";
import { findCssClassesLazy } from "@webpack";
import { Button, Clickable, Menu, Popout, React, useRef } from "@webpack/common";
const { overflowIcon } = findCssClassesLazy("overflowIcon", "forceButtonsShow");
export function NoteBookTabs({ tabs, selectedTabId, onSelectTab }: { tabs: string[], selectedTabId: string, onSelectTab: (tab: string) => void; }) {
const tabBarRef = React.useRef<HTMLDivElement>(null);
const widthRef = React.useRef<number>(0);
const tabWidthMapRef = React.useRef(new Map());
const [overflowedTabs, setOverflowedTabs] = React.useState<string[]>([]);
const resizeObserverRef = React.useRef<ResizeObserver | null>(null);
const [show, setShow] = React.useState(false);
function isNotNullish(value) {
return value !== null && value !== undefined;
}
const handleResize = React.useCallback(() => {
if (!tabBarRef.current) return;
const overflowed = [] as string[];
const totalWidth = tabBarRef.current.clientWidth;
if (totalWidth !== widthRef.current) {
// Thanks to daveyy1 for the help with this
let width = 0;
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
const tabRef = tabWidthMapRef.current.get(tab);
if (!tabRef) continue;
width += tabRef.width;
if (width > totalWidth) {
overflowed.push(tab);
}
}
setOverflowedTabs(overflowed);
}
}, [tabs, selectedTabId]);
React.useEffect(() => {
handleResize();
resizeObserverRef.current = new ResizeObserver(handleResize);
if (tabBarRef.current) resizeObserverRef.current.observe(tabBarRef.current);
return () => {
if (resizeObserverRef.current) resizeObserverRef.current.disconnect();
};
}, [handleResize]);
const TabItem = React.forwardRef(function ({ id, selected, onClick, children }: { id: string, selected: boolean, onClick: () => void, children: React.ReactNode; }, ref) {
return (
<Clickable
className={classes("vc-notebook-tabbar-item", selected ? "vc-notebook-selected" : "")}
data-tab-id={id}
// @ts-expect-error
innerRef={ref}
onClick={onClick}
>
{children}
</Clickable>
);
});
const renderOverflowMenu = React.useCallback((closePopout: () => void) => {
return (
<Menu.Menu
navId="notebook-tabs"
aria-label="Notebook Tabs"
variant="fixed"
onClose={closePopout}
onSelect={closePopout}
>
{tabs.map(tab => {
return overflowedTabs.includes(tab) && selectedTabId !== tab ? (
<Menu.MenuItem
id={tab}
label={tab}
action={() => onSelectTab(tab)}
/>
) : null;
}).filter(isNotNullish)}
</Menu.Menu>
);
}, [tabs, selectedTabId, onSelectTab, overflowedTabs]);
const buttonRef = useRef(null);
return (
<div
className={classes("vc-notebook-tabbar")}
ref={tabBarRef}
>
{tabs.map(tab => {
if (!overflowedTabs.includes(tab)) {
return (
<TabItem
key={tab}
id={tab}
selected={selectedTabId === tab}
ref={(el: HTMLElement | null) => {
const width = tabWidthMapRef.current.get(tab)?.width ?? 0;
tabWidthMapRef.current.set(tab, {
node: el,
width: el ? el.getBoundingClientRect().width : width
});
}}
onClick={selectedTabId !== tab ? () => onSelectTab(tab) : () => { }}
>
{tab}
</TabItem>
);
}
return null;
}).filter(isNotNullish)}
{overflowedTabs.length > 0 && (
<Popout
shouldShow={show}
onRequestClose={() => setShow(false)}
renderPopout={() => renderOverflowMenu(() => setShow(false))}
position="bottom"
align="right"
spacing={0}
targetElementRef={buttonRef}
>
{props => (
<Button
ref={buttonRef}
{...props}
className={"vc-notebook-overflow-chevron"}
size={Button.Sizes.SMALL}
look={Button.Looks.FILLED}
onClick={() => setShow(v => !v)}
>
<SvgOverFlowIcon className={classes(overflowIcon)} width={16} height={16} />
</Button>
)}
</Popout>
)}
</div >
);
}
export function CreateTabBar({ tabs, firstSelectedTab, onChangeTab }) {
const tabKeys = Object.keys(tabs);
const mainTabIndex = tabKeys.indexOf("Main");
if (mainTabIndex !== -1 && mainTabIndex !== 0) {
tabKeys.splice(mainTabIndex, 1);
tabKeys.unshift("Main");
}
const [selectedTab, setSelectedTab] = React.useState(
firstSelectedTab || (tabKeys.length > 0 ? tabKeys[0] : null)
);
const renderSelectedTab = React.useCallback(() => {
const selectedTabId = tabKeys.find(tab => tab === selectedTab);
return selectedTabId;
}, [tabs, selectedTab]);
return {
TabBar: <NoteBookTabs
tabs={tabKeys}
selectedTabId={selectedTab}
onSelectTab={tab => {
setSelectedTab(tab);
if (onChangeTab) onChangeTab(tab);
}} />,
renderSelectedTab,
selectedTab
};
}

View file

@ -5,261 +5,172 @@
*/
import { BaseText } from "@components/BaseText";
import { Button } from "@components/Button";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { quickSelectClasses } from "@equicordplugins/holyNotes";
import HelpIcon from "@equicordplugins/holyNotes/components/icons/HelpIcon";
import NoteButton from "@equicordplugins/holyNotes/components/icons/NoteButton";
import { BookmarkIcon, CircleQuestionIcon, cl } from "@equicordplugins/holyNotes";
import { noteHandler } from "@equicordplugins/holyNotes/NoteHandler";
import { HolyNotes } from "@equicordplugins/holyNotes/types";
import { classes } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { ContextMenuApi, FluxDispatcher, Menu, React, TextInput } from "@webpack/common";
import { Note, Notebook } from "@equicordplugins/holyNotes/types";
import { CloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Clickable, React, Select, TextInput, Tooltip, useState } from "@webpack/common";
import Errors from "./Error";
import HelpModal from "./HelpModal";
import ManageNotebookButton from "./ManageNotebookButton";
import { CreateTabBar } from "./NoteBookTab";
import NotebookCreateModal from "./NotebookCreateModal";
import NotebookDeleteModal from "./NotebookDeleteModal";
import { RenderMessage } from "./RenderMessage";
const renderNotebook = ({
notes, notebook, updateParent, sortDirection, sortType, searchInput, closeModal
}: {
notes: Record<string, HolyNotes.Note>;
notebook: string;
updateParent: () => void;
sortDirection: boolean;
sortType: boolean;
searchInput: string;
closeModal: () => void;
}) => {
let notesArray = Object.values(notes);
const enum SortOption {
NewestAdded,
OldestAdded,
NewestMessage,
OldestMessage
}
if (searchInput) {
const searchLower = searchInput.toLowerCase();
notesArray = notesArray.filter(note =>
note.content?.toLowerCase().includes(searchLower)
);
}
if (!notesArray.length) return <Errors />;
if (sortType) {
notesArray.sort((a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
}
if (sortDirection) notesArray.reverse();
return notesArray.map(note => (
<RenderMessage
key={note.id || notebook}
note={note}
notebook={notebook}
updateParent={updateParent}
fromDeleteModal={false}
closeModal={closeModal}
/>
));
};
export const NoteModal = (props: ModalProps & { onClose: () => void; }) => {
const [sortType, setSortType] = React.useState(true);
const [searchInput, setSearch] = React.useState("");
const [sortDirection, setSortDirection] = React.useState(true);
const [currentNotebook, setCurrentNotebook] = React.useState("Main");
const forceUpdate = React.useReducer(() => ({}), {})[1] as () => void;
const notes = noteHandler.getNotes(currentNotebook);
if (!notes) return <></>;
const notesArray = Object.values(notes);
const noteCount = notesArray.length;
// Filter notes for display count
let filteredCount = noteCount;
if (searchInput) {
const searchLower = searchInput.toLowerCase();
filteredCount = notesArray.filter(note =>
note.content?.toLowerCase().includes(searchLower)
).length;
}
const { TabBar, selectedTab } = CreateTabBar({
tabs: noteHandler.getAllNotes(),
firstSelectedTab: currentNotebook,
onChangeTab: setCurrentNotebook
});
function NotebookTabs({ notebooks, selected, onSelect }: {
notebooks: string[];
selected: string;
onSelect: (tab: string) => void;
}) {
const sorted = [...notebooks].sort((a, b) =>
a === "Main" ? -1 : b === "Main" ? 1 : a.localeCompare(b)
);
return (
<ErrorBoundary>
<ModalRoot {...props} className={classes("vc-notebook")} size={ModalSize.LARGE}>
<Flex className={classes("vc-notebook-flex")} flexDirection="column" style={{ width: "100%", height: "100%" }}>
<div className={classes("vc-notebook-top-section")}>
<ModalHeader className={classes("vc-notebook-header-main")}>
<Flex alignItems="center" style={{ gap: "8px", flex: 1 }}>
<NoteButton className={classes("vc-notebook-icon")} />
<Flex flexDirection="column" style={{ gap: "4px", flex: 1 }}>
<BaseText
size="lg"
weight="semibold"
className={classes("vc-notebook-heading")}>
Notebook
</BaseText>
<BaseText
size="sm"
className={classes("vc-notebook-count")}>
{searchInput
? `${filteredCount} of ${noteCount} ${noteCount === 1 ? "note" : "notes"}`
: `${noteCount} ${noteCount === 1 ? "note" : "notes"}`
}
</BaseText>
</Flex>
</Flex>
<Flex alignItems="center" style={{ gap: "8px" }}>
<div
className={classes("vc-notebook-help-button")}
onClick={() => openModal(HelpModal)}
role="button"
tabIndex={0}
aria-label="Help"
onKeyDown={e => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
openModal(HelpModal);
}
}}>
<HelpIcon />
</div>
<ModalCloseButton onClick={props.onClose} />
</Flex>
</ModalHeader>
<div className={classes("vc-notebook-search-container")}>
<TextInput
autoFocus={false}
placeholder="Search notes..."
value={searchInput}
onChange={e => setSearch(e)}
className={classes("vc-notebook-search-input")}
/>
</div>
<div className={classes("vc-notebook-tabbar-container")}>
{TabBar}
</div>
</div>
<ModalContent
className={classes("vc-notebook-content")}
style={{
flex: 1,
overflowY: "auto",
overflowX: "hidden",
scrollBehavior: "smooth"
}}>
<ErrorBoundary>
{renderNotebook({
notes,
notebook: currentNotebook,
updateParent: () => forceUpdate(),
sortDirection: sortDirection,
sortType: sortType,
searchInput: searchInput,
closeModal: props.onClose,
})}
</ErrorBoundary>
</ModalContent>
</Flex>
<ModalFooter className={classes("vc-notebook-footer")}>
<Flex style={{ width: "100%", justifyContent: "space-between", alignItems: "center" }}>
<ManageNotebookButton notebook={currentNotebook} setNotebook={setCurrentNotebook} />
<div className={classes("sort-button-container")}>
<Flex
alignItems="center"
className={quickSelectClasses.quickSelect}
role="button"
tabIndex={0}
aria-label="Sort options"
onClick={(event: React.MouseEvent<HTMLDivElement>) => {
ContextMenuApi.openContextMenu(event, () => (
<Menu.Menu
navId="sort-menu"
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
aria-label="Sort options"
>
<Menu.MenuItem
label="Ascending / Date Added"
id="ada"
action={() => {
setSortDirection(true);
setSortType(true);
}}
/>
<Menu.MenuItem
label="Ascending / Message Date"
id="amd"
action={() => {
setSortDirection(true);
setSortType(false);
}}
/>
<Menu.MenuItem
label="Descending / Date Added"
id="dda"
action={() => {
setSortDirection(false);
setSortType(true);
}}
/>
<Menu.MenuItem
label="Descending / Message Date"
id="dmd"
action={() => {
setSortDirection(false);
setSortType(false);
}}
/>
</Menu.Menu>
));
}}
onKeyDown={e => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
const el = e.currentTarget as HTMLElement;
const rect = el.getBoundingClientRect();
const evt = {
clientX: rect.left + rect.width / 2,
clientY: rect.top + rect.height / 2
} as any;
ContextMenuApi.openContextMenu(evt, () => (
<Menu.Menu
navId="sort-menu"
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
aria-label="Sort options"
>
<Menu.MenuItem label="Ascending / Date Added" id="ada" action={() => { setSortDirection(true); setSortType(true); }} />
<Menu.MenuItem label="Ascending / Message Date" id="amd" action={() => { setSortDirection(true); setSortType(false); }} />
<Menu.MenuItem label="Descending / Date Added" id="dda" action={() => { setSortDirection(false); setSortType(true); }} />
<Menu.MenuItem label="Descending / Message Date" id="dmd" action={() => { setSortDirection(false); setSortType(false); }} />
</Menu.Menu>
));
}
}}
>
<BaseText className={quickSelectClasses.quickSelectLabel}>Sort:</BaseText>
<Flex style={{ flexGrow: 0 }} alignItems="center" className={quickSelectClasses.quickSelectClick}>
<BaseText className={quickSelectClasses.quickSelectValue}>
{sortDirection ? "Asc" : "Desc"} /{" "}
{sortType ? "Date Added" : "Msg Date"}
</BaseText>
<div className={quickSelectClasses.quickSelectArrow} aria-hidden="true" />
</Flex>
</Flex>
</div>
</Flex>
</ModalFooter>
</ModalRoot>
</ErrorBoundary>
<div className={cl("tabs")}>
{sorted.map(name => (
<Clickable
key={name}
className={cl("tab", selected === name && "tab-selected")}
onClick={() => onSelect(name)}
>
{name}
</Clickable>
))}
</div>
);
};
}
function sortNotes(notes: Note[], sort: SortOption): Note[] {
const sorted = [...notes];
switch (sort) {
case SortOption.NewestAdded:
return sorted;
case SortOption.OldestAdded:
return sorted.reverse();
case SortOption.NewestMessage:
return sorted.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
case SortOption.OldestMessage:
return sorted.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
}
}
function filterNotes(notes: Notebook, search: string): Note[] {
const arr = Object.values(notes);
if (!search) return arr;
const q = search.toLowerCase();
return arr.filter(n => n.content?.toLowerCase().includes(q));
}
export function NoteModal({ onClose, transitionState }: ModalProps) {
const [sort, setSort] = useState(SortOption.NewestAdded);
const [search, setSearch] = useState("");
const [notebook, setNotebook] = useState("Main");
const [, forceUpdate] = React.useReducer(x => x + 1, 0);
const notes = noteHandler.getNotes(notebook);
if (!notes) return null;
const filtered = filterNotes(notes, search);
const sorted = sortNotes(filtered, sort);
const total = Object.keys(notes).length;
const isMain = notebook === "Main";
return (
<ModalRoot transitionState={transitionState} size={ModalSize.LARGE}>
<ModalHeader separator={false} className={cl("header")}>
<div className={cl("header-content")}>
<Flex alignItems="center" style={{ gap: "12px" }}>
<BookmarkIcon size="md" />
<BaseText tag="h2" size="lg" weight="semibold" className={cl("title")}>
Notebook
</BaseText>
</Flex>
<BaseText size="sm" className={cl("description")}>
{search ? `${filtered.length} of ${total}` : total} {total === 1 ? "note" : "notes"}
</BaseText>
</div>
<div className={cl("header-trailing")}>
<CloseButton onClick={onClose} />
</div>
</ModalHeader>
<div className={cl("controls")}>
<TextInput
placeholder="Search notes..."
value={search}
onChange={setSearch}
/>
<Select
options={[
{ label: "Newest First", value: SortOption.NewestAdded, default: true },
{ label: "Oldest First", value: SortOption.OldestAdded },
{ label: "Newest Msg", value: SortOption.NewestMessage },
{ label: "Oldest Msg", value: SortOption.OldestMessage },
]}
serialize={String}
select={setSort}
isSelected={v => v === sort}
closeOnSelect
/>
</div>
<NotebookTabs
notebooks={Object.keys(noteHandler.getAllNotes())}
selected={notebook}
onSelect={setNotebook}
/>
<ModalContent className={cl("content")}>
<ErrorBoundary>
{sorted.length ? sorted.map(note => (
<RenderMessage
key={note.id}
note={note}
notebook={notebook}
updateParent={forceUpdate}
fromDeleteModal={false}
closeModal={onClose}
/>
)) : <Errors />}
</ErrorBoundary>
</ModalContent>
<ModalFooter>
<Flex style={{ justifyContent: "space-between", alignItems: "center", width: "100%" }}>
<Tooltip text="Help">
{({ onMouseEnter, onMouseLeave }) => (
<Clickable
className={cl("icon-button")}
onClick={() => openModal(props => <HelpModal {...props} />)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<CircleQuestionIcon size="sm" />
</Clickable>
)}
</Tooltip>
<Button
variant={isMain ? "primary" : "dangerPrimary"}
onClick={() => isMain
? openModal(props => <NotebookCreateModal {...props} />)
: openModal(props => <NotebookDeleteModal {...props} notebook={notebook} onChangeTab={setNotebook} />)
}
>
{isMain ? "Create Notebook" : "Delete Notebook"}
</Button>
</Flex>
</ModalFooter>
</ModalRoot>
);
}

View file

@ -4,37 +4,52 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { HeadingSecondary } from "@components/Heading";
import { BaseText } from "@components/BaseText";
import { Button } from "@components/Button";
import { cl } from "@equicordplugins/holyNotes";
import { noteHandler } from "@equicordplugins/holyNotes/NoteHandler";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { Button, React, TextInput } from "@webpack/common";
import { CloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { TextInput, useState } from "@webpack/common";
export default (props: ModalProps & { onClose: () => void; }) => {
const [notebookName, setNotebookName] = React.useState("");
export default function NotebookCreateModal({ onClose, transitionState }: ModalProps) {
const [name, setName] = useState("");
const handleCreateNotebook = React.useCallback(() => {
if (notebookName !== "") noteHandler.newNoteBook(notebookName);
props.onClose();
}, [notebookName]);
const handleCreate = () => {
if (!name.trim()) return;
noteHandler.newNoteBook(name.trim());
onClose();
};
return (
<div>
<ModalRoot className="vc-create-notebook" size={ModalSize.SMALL} {...props}>
<ModalHeader className="vc-notebook-header">
<HeadingSecondary>Create Notebook</HeadingSecondary>
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent>
<TextInput
value={notebookName}
placeholder="Notebook Name"
onChange={value => setNotebookName(value)}
style={{ marginBottom: "10px" }} />
</ModalContent>
<ModalFooter>
<Button onClick={handleCreateNotebook} color={Button.Colors.GREEN}>Create Notebook</Button>
</ModalFooter>
</ModalRoot>
</div>
<ModalRoot transitionState={transitionState} size={ModalSize.SMALL} className={cl("create-modal")}>
<ModalHeader separator={false} className={cl("header")}>
<div className={cl("header-content")}>
<BaseText tag="h2" size="lg" weight="semibold" className={cl("title")}>
Create Notebook
</BaseText>
<BaseText size="sm" className={cl("description")}>
Enter a name for your new notebook
</BaseText>
</div>
<div className={cl("header-trailing")}>
<CloseButton onClick={onClose} />
</div>
</ModalHeader>
<ModalContent className={cl("content")}>
<TextInput
value={name}
placeholder="Notebook Name"
onChange={setName}
/>
</ModalContent>
<ModalFooter>
<Button variant="primary" onClick={handleCreate}>
Create
</Button>
<Button variant="secondary" onClick={onClose}>
Cancel
</Button>
</ModalFooter>
</ModalRoot>
);
};
}

View file

@ -4,16 +4,23 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { BaseText } from "@components/BaseText";
import { Button } from "@components/Button";
import ErrorBoundary from "@components/ErrorBoundary";
import { HeadingSecondary } from "@components/Heading";
import { cl } from "@equicordplugins/holyNotes";
import { noteHandler } from "@equicordplugins/holyNotes/NoteHandler";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { Button, React } from "@webpack/common";
import { CloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { React } from "@webpack/common";
import Error from "./Error";
import EmptyNotebook from "./Error";
import { RenderMessage } from "./RenderMessage";
export default ({ onClose, notebook, onChangeTab, ...props }: ModalProps & { onClose: () => void; notebook: string; onChangeTab: React.Dispatch<React.SetStateAction<string>>; }) => {
interface Props extends ModalProps {
notebook: string;
onChangeTab: React.Dispatch<React.SetStateAction<string>>;
}
export default function NotebookDeleteModal({ onClose, transitionState, notebook, onChangeTab }: Props) {
const notes = noteHandler.getNotes(notebook);
const handleDelete = () => {
@ -22,38 +29,40 @@ export default ({ onClose, notebook, onChangeTab, ...props }: ModalProps & { onC
noteHandler.deleteNotebook(notebook);
};
const noteList = notes ? Object.values(notes) : [];
return (
<ModalRoot
{...props}
className="vc-delete-notebook"
size={ModalSize.LARGE}>
<ModalHeader>
<HeadingSecondary>Confirm Deletion</HeadingSecondary>
<ModalCloseButton onClick={onClose} />
<ModalRoot transitionState={transitionState} size={ModalSize.LARGE}>
<ModalHeader separator={false} className={cl("header")}>
<div className={cl("header-content")}>
<BaseText tag="h2" size="lg" weight="semibold" className={cl("title")}>
Delete "{notebook}"?
</BaseText>
<BaseText size="sm" className={cl("description")}>
{noteList.length} {noteList.length === 1 ? "note" : "notes"} will be deleted
</BaseText>
</div>
<div className={cl("header-trailing")}>
<CloseButton onClick={onClose} />
</div>
</ModalHeader>
<ModalContent>
<ModalContent className={cl("content")}>
<ErrorBoundary>
{notes && Object.keys(notes).length > 0 ? (
Object.values(notes).map(note => (
<RenderMessage
key={notebook}
note={note}
notebook={notebook}
fromDeleteModal={true} />
))
) : (
<Error />
)}
{noteList.length ? noteList.map(note => (
<RenderMessage
key={note.id}
note={note}
notebook={notebook}
fromDeleteModal
/>
)) : <EmptyNotebook />}
</ErrorBoundary>
</ModalContent>
<ModalFooter>
<Button
color={Button.Colors.RED}
onClick={handleDelete}
>
DELETE
<Button variant="dangerPrimary" onClick={handleDelete}>
Delete Notebook
</Button>
</ModalFooter>
</ModalRoot>
);
};
}

View file

@ -5,93 +5,89 @@
*/
import { CopyIcon, DeleteIcon, IDIcon, LinkIcon, OpenExternalIcon } from "@components/Icons";
import { ChannelMessage, messageClasses, MessageType } from "@equicordplugins/holyNotes";
import { ChannelMessage, ChannelRecord, messageClasses, MessageRecord } from "@equicordplugins/holyNotes";
import { noteHandler } from "@equicordplugins/holyNotes/NoteHandler";
import { HolyNotes } from "@equicordplugins/holyNotes/types";
import { Note } from "@equicordplugins/holyNotes/types";
import { copyToClipboard } from "@utils/clipboard";
import { fetchUserProfile } from "@utils/discord";
import { proxyLazy } from "@utils/lazy";
import { classes } from "@utils/misc";
import { ModalProps } from "@utils/modal";
import { ContextMenuApi, FluxDispatcher, Menu, NavigationRouter, React, UserStore } from "@webpack/common";
import { Channel } from "../..";
import { ContextMenuApi, FluxDispatcher, Menu, NavigationRouter, useEffect, useReducer, useRef, UserStore } from "@webpack/common";
const UserRecord = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
export const RenderMessage = ({
note,
notebook,
updateParent,
fromDeleteModal,
closeModal,
}: {
note: HolyNotes.Note;
interface Props {
note: Note;
notebook: string;
updateParent?: () => void;
fromDeleteModal: boolean;
closeModal?: () => void;
}) => {
const isHoldingDeleteRef = React.useRef(false);
const [, forceUpdate] = React.useReducer(x => x + 1, 0);
}
React.useEffect(() => {
const user = UserStore.getUser(note.author.id);
if (!user) {
export function RenderMessage({ note, notebook, updateParent, fromDeleteModal, closeModal }: Props) {
const isHoldingDeleteRef = useRef(false);
const [, forceUpdate] = useReducer(x => x + 1, 0);
useEffect(() => {
if (!UserStore.getUser(note.author.id)) {
fetchUserProfile(note.author.id);
}
}, [note.author.id]);
const author = UserStore.getUser(note.author.id) ?? note.author;
React.useEffect(() => {
const deleteHandler = (e: KeyboardEvent) => {
if (e.key.toLowerCase() !== "delete") return;
const newState = e.type === "keydown";
if (isHoldingDeleteRef.current !== newState) {
isHoldingDeleteRef.current = newState;
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
if (e.key !== "Delete") return;
const isDown = e.type === "keydown";
if (isHoldingDeleteRef.current !== isDown) {
isHoldingDeleteRef.current = isDown;
forceUpdate();
}
};
document.addEventListener("keydown", deleteHandler);
document.addEventListener("keyup", deleteHandler);
document.addEventListener("keydown", handleKey);
document.addEventListener("keyup", handleKey);
return () => {
document.removeEventListener("keydown", deleteHandler);
document.removeEventListener("keyup", deleteHandler);
document.removeEventListener("keydown", handleKey);
document.removeEventListener("keyup", handleKey);
};
}, []);
const author = UserStore.getUser(note.author.id) ?? note.author;
const handleClick = () => {
if (isHoldingDeleteRef.current && !fromDeleteModal) {
noteHandler.deleteNote(note.id, notebook);
updateParent?.();
}
};
const handleContextMenu = (e: React.MouseEvent) => {
if (fromDeleteModal) return;
ContextMenuApi.openContextMenu(e, () => (
<NoteContextMenu
note={note}
notebook={notebook}
updateParent={updateParent}
closeModal={closeModal}
/>
));
};
const message = new MessageRecord({
...note,
author: new UserRecord({ ...author, bot: true }),
timestamp: new Date(note.timestamp),
embeds: note.embeds?.map(embed =>
embed.timestamp ? { ...embed, timestamp: new Date(embed.timestamp) } : embed
),
});
return (
<div
className="vc-holy-note"
style={{
marginBottom: "8px",
marginTop: "8px",
paddingTop: "4px",
paddingBottom: "4px",
}}
onClick={() => {
if (isHoldingDeleteRef.current && !fromDeleteModal) {
noteHandler.deleteNote(note.id, notebook);
updateParent?.();
}
}}
onContextMenu={(event: any) => {
if (!fromDeleteModal)
// @ts-ignore
return ContextMenuApi.openContextMenu(event, (props: any) => (
// @ts-ignore
<NoteContextMenu
{...Object.assign({}, props, { onClose: close })}
note={note}
notebook={notebook}
updateParent={updateParent}
closeModal={closeModal}
/>
));
}}
style={{ margin: "8px 0", padding: "4px 0" }}
onClick={handleClick}
onContextMenu={handleContextMenu}
>
<ChannelMessage
className={classes("vc-holy-render", messageClasses?.message, messageClasses?.groupStart, messageClasses?.cozyMessage)}
@ -102,41 +98,22 @@ export const RenderMessage = ({
isHighlight={false}
isLastItem={false}
renderContentOnly={false}
// @ts-ignore
channel={new Channel({ id: "holy-notes" })}
message={
new MessageType(
Object.assign(
{ ...note },
{
author: new UserRecord({ ...author, bot: true }),
timestamp: new Date(note?.timestamp),
// @ts-ignore
embeds: note?.embeds?.map((embed: { timestamp: string | number | Date; }) =>
embed.timestamp
? Object.assign(embed, {
timestamp: new Date(embed.timestamp),
})
: embed,
),
},
),
)
}
channel={new ChannelRecord({ id: "holy-notes" })}
message={message}
/>
</div>
);
};
}
const NoteContextMenu = (
props: ModalProps & {
updateParent?: () => void;
notebook: string;
note: HolyNotes.Note;
closeModal?: () => void;
}) => {
const { note, notebook, updateParent, closeModal } = props;
interface ContextMenuProps {
note: Note;
notebook: string;
updateParent?: () => void;
closeModal?: () => void;
}
function NoteContextMenu({ note, notebook, updateParent, closeModal }: ContextMenuProps) {
const allNotebooks = Object.keys(noteHandler.getAllNotes());
return (
<Menu.Menu
@ -147,64 +124,57 @@ const NoteContextMenu = (
<Menu.MenuItem
label="Jump To Message"
id="jump"
icon={OpenExternalIcon}
action={() => {
NavigationRouter.transitionTo(`/channels/${note.guild_id ?? "@me"}/${note.channel_id}/${note.id}`);
NavigationRouter.transitionTo(`/channels/${note.guild_id || "@me"}/${note.channel_id}/${note.id}`);
closeModal?.();
}}
icon={OpenExternalIcon}
/>
<Menu.MenuItem
label="Copy Text"
id="copy-text"
action={() => copyToClipboard(note.content)}
icon={CopyIcon}
action={() => copyToClipboard(note.content)}
/>
{note?.attachments.length ? (
{note.attachments.length > 0 && (
<Menu.MenuItem
label="Copy Attachment URL"
id="copy-url"
action={() => copyToClipboard(note.attachments[0].url)}
icon={LinkIcon}
/>) : null}
action={() => copyToClipboard(note.attachments[0].url)}
/>
)}
<Menu.MenuItem
label="Copy ID"
id="copy-id"
action={() => copyToClipboard(note.id)}
icon={IDIcon}
action={() => copyToClipboard(note.id)}
/>
{Object.keys(noteHandler.getAllNotes()).length !== 1 ? (
<Menu.MenuItem
label="Move Note"
id="move-note"
>
{Object.keys(noteHandler.getAllNotes()).map((key: string) => {
if (key !== notebook) {
return (
<Menu.MenuItem
key={key}
label={`Move to ${key}`}
id={key}
action={() => {
noteHandler.moveNote(note, notebook, key);
updateParent?.();
}}
/>
);
}
})}
{allNotebooks.length > 1 && (
<Menu.MenuItem label="Move Note" id="move-note">
{allNotebooks.filter(k => k !== notebook).map(k => (
<Menu.MenuItem
key={k}
label={`Move to ${k}`}
id={k}
action={() => {
noteHandler.moveNote(note, notebook, k);
updateParent?.();
}}
/>
))}
</Menu.MenuItem>
) : null}
)}
<Menu.MenuItem
color="danger"
label="Delete Note"
id="delete"
icon={DeleteIcon}
action={() => {
noteHandler.deleteNote(note.id, notebook);
updateParent?.();
}}
icon={DeleteIcon}
/>
</Menu.Menu>
);
};
}

View file

@ -22,30 +22,31 @@ import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { HeaderBarButton } from "@api/HeaderBar";
import { DataStore } from "@api/index";
import { EquicordDevs } from "@utils/constants";
import { classes } from "@utils/misc";
import { classNameFactory } from "@utils/css";
import { openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import definePlugin, { IconComponent } from "@utils/types";
import { Message } from "@vencord/discord-types";
import { findByCodeLazy, findComponentByCodeLazy, findCssClassesLazy } from "@webpack";
import { findByCodeLazy, findComponentByCodeLazy, findCssClassesLazy, findExportedComponentLazy } from "@webpack";
import { ChannelStore, Menu } from "@webpack/common";
import { Popover as NoteButtonPopover, Popover } from "./components/icons/NoteButton";
import { NoteModal } from "./components/modals/Notebook";
import { noteHandler, noteHandlerCache } from "./NoteHandler";
import { DataStoreToCache, HolyNoteStore } from "./utils";
export const MessageType = findByCodeLazy("isEdited(){");
export const iconClasses = findCssClassesLazy("iconWrapper", "clickable");
export const resultsClasses = findCssClassesLazy("emptyResultsWrap", "emptyResultsContent", "errorImage", "emptyResultsText", "noResultsImage", "alt");
export const quickSelectClasses = findCssClassesLazy("quickSelect", "quickSelectLabel", "quickSelectClick", "quickSelectValue", "quickSelectArrow");
export const cl = classNameFactory("vc-notebook-");
export const CircleQuestionIcon = findExportedComponentLazy("CircleQuestionIcon");
export const BookmarkIconLazy = findExportedComponentLazy("BookmarkIcon");
export const BookmarkIcon: IconComponent = props => <BookmarkIconLazy {...props} />;
export const MessageRecord = findByCodeLazy("isEdited(){");
export const messageClasses = findCssClassesLazy("message", "groupStart", "cozyMessage");
export const Channel = findByCodeLazy("computeLurkerPermissionsAllowList(){");
export const resultsClasses = findCssClassesLazy("emptyResultsWrap", "emptyResultsContent", "errorImage", "emptyResultsText", "noResultsImage", "alt");
export const ChannelRecord = findByCodeLazy("computeLurkerPermissionsAllowList(){");
export const ChannelMessage = findComponentByCodeLazy("Message must not be a thread");
const messageContextMenuPatch: NavContextMenuPatchCallback = async (children, { message }: { message: Message; }) => {
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => {
children.push(
<Menu.MenuItem label="Add Message To" id="add-message-to-note">
{Object.keys(noteHandler.getAllNotes()).map((notebook: string, index: number) => (
<Menu.MenuItem label="Note Message" id="note-message">
{Object.keys(noteHandler.getAllNotes()).map(notebook => (
<Menu.MenuItem
key={notebook}
label={notebook}
@ -62,8 +63,7 @@ function ToolBarHeader() {
<HeaderBarButton
tooltip="Holy Notes"
position="bottom"
className={classes("vc-note-button", iconClasses.iconWrapper, iconClasses.clickable)}
icon={Popover}
icon={BookmarkIcon}
onClick={() => openModal(props => <NoteModal {...props} />)}
/>
);
@ -71,39 +71,43 @@ function ToolBarHeader() {
export default definePlugin({
name: "HolyNotes",
description: "Holy Notes allows you to save messages",
description: "Save messages as notes to revisit later",
authors: [EquicordDevs.Wolfie],
toolboxActions: {
async "Open Notes"() {
"Open Notes"() {
openModal(props => <NoteModal {...props} />);
}
},
contextMenus: {
"message": messageContextMenuPatch
message: messageContextMenuPatch
},
headerBarButton: {
icon: Popover,
icon: BookmarkIcon,
render: ToolBarHeader
},
messagePopoverButton: {
icon: NoteButtonPopover,
icon: BookmarkIcon,
render(message) {
return {
label: "Save Note",
icon: NoteButtonPopover,
message: message,
icon: BookmarkIcon,
message,
channel: ChannelStore.getChannel(message.channel_id),
onClick: () => noteHandler.addNote(message, "Main")
};
}
},
async start() {
if (await DataStore.keys(HolyNoteStore).then(keys => !keys.includes("Main"))) return noteHandler.newNoteBook("Main");
const keys = await DataStore.keys(HolyNoteStore);
if (!keys.includes("Main")) {
noteHandler.newNoteBook("Main");
return;
}
if (!noteHandlerCache.has("Main")) await DataStoreToCache();
},
});

View file

@ -1,117 +1,87 @@
/* stylelint-disable shorthand-property-no-redundant-values */
/* stylelint-disable length-zero-no-unit */
.vc-notebook-search {
flex: auto;
margin-right: 15px;
margin-bottom: 10px;
background-color: var(--background-surface-higher);
}
.vc-notebook-header {
margin-bottom: 10px;
padding: 16px 16px 10px 16px;
align-items: flex-start;
position: relative;
padding: 16px 16px 0 24px;
}
.vc-notebook-header-main {
padding: 16px 16px 12px 16px;
background-color: var(--background-surface-higher);
box-shadow: none !important;
.vc-notebook-header-content {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.vc-notebook-heading {
max-width: none;
margin: 0;
padding: 0;
font-size: 20px;
font-weight: 600;
line-height: 24px;
color: var(--text-default);
.vc-notebook-header-trailing {
position: absolute;
top: 8px;
right: 8px;
}
.vc-notebook-count {
font-size: 12px;
.vc-notebook-title {
color: var(--text-strong);
}
.vc-notebook-description {
color: var(--text-subtle);
}
.vc-notebook-controls {
display: grid;
gap: 10px;
grid-template-columns: 1fr 150px;
padding: 16px 24px;
}
.vc-notebook-tabs {
display: flex;
gap: 24px;
padding: 0 24px;
border-bottom: 1px solid var(--border-subtle);
}
.vc-notebook-tab {
padding: 8px 0;
font-size: 14px;
font-weight: 500;
line-height: 16px;
color: var(--text-muted);
}
.vc-notebook-flex {
overflow: hidden;
}
.vc-notebook-flex .vc-help-icon {
width: 20px;
height: 20px;
opacity: 0.6;
color: var(--text-subtle);
cursor: pointer;
padding: 0;
margin: 0;
color: var(--icon-default);
transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.15s ease, border-color 0.15s ease;
}
.vc-notebook-icon {
width: 24px;
height: 24px;
cursor: default;
margin: 0;
.vc-notebook-tab:hover {
color: var(--text-default);
flex-shrink: 0;
border-bottom-color: var(--text-subtle);
}
.vc-notebook-help-button {
width: 20px;
height: 20px;
.vc-notebook-tab-selected {
color: var(--text-default);
border-bottom-color: var(--control-brand-foreground);
}
.vc-notebook-content {
padding: 16px 24px;
}
.vc-notebook-create-modal [class*="footer"] {
gap: 8px;
}
.vc-notebook-icon-button {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s ease-in-out;
padding: 4px;
color: var(--icon-subtle);
border-radius: 4px;
transition: color 0.15s ease;
}
.vc-notebook-help-button:hover {
opacity: 1;
}
.vc-notebook-help-button svg {
width: 20px;
height: 20px;
.vc-notebook-icon-button:hover {
color: var(--icon-default);
}
.vc-notebook-display-left {
flex: auto;
display: flex;
}
.vc-notebook-search-container {
padding: 0 16px 12px 16px;
background-color: var(--background-surface-higher);
}
.vc-notebook-search-input {
width: 100%;
background-color: var(--input-background-default);
}
.vc-help-markdown {
margin-top: 6px;
}
.vc-help-markdown hr {
border: 0;
height: 1px;
margin: 12px 0px 16px 0px;
background-color: var(--border-normal);
}
.vc-help-markdown .vc-help-protip {
font-weight: 600;
color: var(--text-feedback-positive);
}
.vc-holy-note [class*="buttonContainer"] {
display: none;
}
@ -120,100 +90,6 @@
list-style-type: none;
}
.vc-notebook-tabbar {
border-bottom: 1px solid var(--border-normal);
background-color: var(--background-surface-higher);
white-space: nowrap;
overflow: hidden;
height: 36px;
flex-shrink: 0;
}
.vc-notebook-overflow-chevron {
display: inline-block;
padding: 0 6px 2px;
height: 16px;
position: relative;
top: -10px;
}
.vc-notebook-tabbar-container {
border-bottom: 1px solid var(--border-normal);
margin: 0 16px;
padding: 0;
background-color: var(--background-surface-higher);
}
.vc-notebook-tabbar-item {
display: inline-block;
padding: 8px 12px;
margin-bottom: -1px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
line-height: 20px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
color: var(--interactive-text-default);
border-bottom-width: 2px;
border-bottom-style: solid;
/* stylelint-disable-next-line declaration-block-no-redundant-longhand-properties */
border-bottom-color: transparent;
transition: color 0.2s ease-in-out, border-bottom-color 0.2s ease-in-out, font-weight 0.2s ease-in-out;
}
.vc-notebook-selected {
border-bottom-width: 3px;
border-bottom-color: var(--control-brand-foreground);
color: var(--text-default);
font-weight: 600;
}
.vc-notebook-tabbar-item:hover {
color: var(--interactive-text-hover);
}
.vc-notebook-tabbar-item:hover:not(.vc-notebook-selected) {
border-bottom-color: var(--interactive-text-hover);
}
.vc-notebook-top-section {
margin-bottom: 0;
z-index: 1;
background-color: var(--background-surface-higher);
}
.vc-notebook-content {
padding: 16px;
background-color: var(--background-surface-high);
}
.vc-notebook-footer {
padding: 16px;
background-color: var(--background-surface-higher);
border-top: 1px solid var(--border-normal);
}
.vc-help-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
}
.vc-help-modal-footer {
display: flex;
flex-wrap: wrap;
gap: 10px;
width: 100%;
}
.vc-help-markdown span[class*="statusTag"] {
display: inline-block;
width: auto;
}
.vc-holy-note [class*="botText"] {
font-size: 0 !important;
line-height: 0;
@ -221,16 +97,24 @@
.vc-holy-note [class*="botText"]::after {
content: "NOTE";
font-size: 0.675rem;
font-size: 0.625rem;
line-height: 1.3;
font-weight: 800;
font-weight: 600;
font-family: var(--font-display);
color: inherit;
letter-spacing: 0.02em;
text-transform: uppercase;
}
.sort-button-container {
.vc-help-markdown {
display: flex;
align-items: center;
}
flex-direction: column;
gap: 4px;
}
.vc-help-markdown hr {
border: none;
height: 1px;
margin: 12px 0;
background-color: var(--border-subtle);
}

View file

@ -5,43 +5,34 @@
*/
import { Embed, MessageAttachment, MessageReaction } from "@vencord/discord-types";
import { StickerFormatType } from "@vencord/discord-types/enums";
export declare namespace Discord {
export interface Sticker {
format_type: number;
id: string;
name: string;
}
export interface Attachment extends MessageAttachment {
sensitive: boolean;
}
export interface Reaction extends MessageReaction {
burst_colors: string[];
borst_count: number;
count_details: { burst: number; normal: number; };
me_burst: boolean;
}
export interface NoteAuthor {
id: string;
avatar: string;
discriminator: string;
username: string;
}
export declare namespace HolyNotes {
export interface Note {
id: string;
channel_id: string;
guild_id: string;
content: string;
author: {
id: string;
avatar: string;
discriminator: string;
username: string;
};
flags: number;
timestamp: string;
attachments: Discord.Attachment[];
embeds: Embed[];
reactions: Discord.Reaction[];
stickerItems: Discord.Sticker[];
}
export interface NoteStickerItem {
format_type: StickerFormatType;
id: string;
name: string;
}
export interface Note {
id: string;
channel_id: string;
guild_id: string;
content: string;
author: NoteAuthor;
flags: number;
timestamp: string;
attachments: MessageAttachment[];
embeds: Embed[];
reactions: MessageReaction[];
stickerItems: NoteStickerItem[];
}
export type Notebook = Record<string, Note>;
export type AllNotebooks = Record<string, Notebook>;

View file

@ -9,98 +9,83 @@ import { DataStore } from "@api/index";
import { Toasts } from "@webpack/common";
import { noteHandler, noteHandlerCache } from "./NoteHandler";
import { HolyNotes } from "./types";
import { Notebook } from "./types";
export const HolyNoteStore = createStore("HolyNoteData", "HolyNoteStore");
export async function saveCacheToDataStore(key: string, value?: HolyNotes.Note[]) {
await DataStore.set(key, value, HolyNoteStore);
export function saveCacheToDataStore(key: string, value: Notebook) {
DataStore.set(key, value, HolyNoteStore);
}
export async function deleteCacheFromDataStore(key: string) {
await DataStore.del(key, HolyNoteStore);
}
export async function getFormatedEntries() {
const data = await DataStore.entries(HolyNoteStore);
const notebooks: Record<string, HolyNotes.Note> = {};
data.forEach(function (note) {
notebooks[note[0].toString()] = note[1];
});
return notebooks;
export function deleteCacheFromDataStore(key: string) {
DataStore.del(key, HolyNoteStore);
}
export async function DataStoreToCache() {
const data = await DataStore.entries(HolyNoteStore);
data.forEach(function (note) {
noteHandlerCache.set(note[0].toString(), note[1]);
});
const entries = await DataStore.entries(HolyNoteStore);
for (const [key, value] of entries) {
noteHandlerCache.set(String(key), value as Notebook);
}
}
export async function DeleteEntireStore() {
await DataStore.clear(HolyNoteStore);
return noteHandler.newNoteBook("Main", true);
noteHandler.newNoteBook("Main", true);
}
export async function downloadNotes() {
const filename = "notes.json";
const exportData = await noteHandler.exportNotes();
const data = JSON.stringify(exportData, null, 2);
const data = JSON.stringify(noteHandler.exportNotes(), null, 2);
if (IS_VESKTOP || IS_WEB) {
if (IS_VESKTOP || IS_EQUIBOP || IS_WEB) {
const file = new File([data], filename, { type: "application/json" });
const a = document.createElement("a");
a.href = URL.createObjectURL(file);
a.download = filename;
document.body.appendChild(a);
a.click();
setImmediate(() => {
setTimeout(() => {
URL.revokeObjectURL(a.href);
document.body.removeChild(a);
});
}, 0);
} else {
DiscordNative.fileManager.saveWithDialog(data, filename);
}
}
export async function uploadNotes() {
if (IS_VESKTOP || IS_WEB) {
if (IS_VESKTOP || IS_EQUIBOP || IS_WEB) {
const input = document.createElement("input");
input.type = "file";
input.style.display = "none";
input.accept = "application/json";
input.onchange = async () => {
input.style.display = "none";
input.onchange = () => {
const file = input.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async () => {
reader.onload = () => {
try {
await noteHandler.importNotes(reader.result as unknown as HolyNotes.Note[]);
} catch (err) {
console.error(err);
noteHandler.importNotes(reader.result as string);
} catch {
Toasts.show({
id: Toasts.genId(),
message: "Invalid JSON.",
message: "Failed to import notes",
type: Toasts.Type.FAILURE,
});
}
};
reader.readAsText(file);
};
document.body.appendChild(input);
input.click();
setImmediate(() => document.body.removeChild(input));
setTimeout(() => document.body.removeChild(input), 0);
} else {
const [file] = await DiscordNative.fileManager.openFiles({ filters: [{ name: "notes", extensions: ["json"] }] });
const [file] = await DiscordNative.fileManager.openFiles({
filters: [{ name: "notes", extensions: ["json"] }]
});
if (file) {
await noteHandler.importNotes(new TextDecoder().decode(file.data) as unknown as HolyNotes.Note[]);
noteHandler.importNotes(new TextDecoder().decode(file.data));
}
}
}

View file

@ -14,6 +14,7 @@ import { TooltipContainer } from "@components/TooltipContainer";
import { copyWithToast, getIntlMessage } from "@utils/discord";
import { Logger } from "@utils/Logger";
import {
CloseButton,
ModalContent,
ModalFooter,
ModalHeader,
@ -41,7 +42,6 @@ import {
import { cssColors, getCssColorKeys, iconSizes, iconSizesInPx } from "../utils";
const logger = new Logger("IconViewer");
const CloseButton = findComponentByCodeLazy("CLOSE_BUTTON_LABEL");
const BugIcon = findComponentByCodeLazy("1.1.27.1.37 0a6.66 6.6");
const FORMAT_EXTENSIONS: Record<string, string> = {

View file

@ -15,7 +15,7 @@ import { PluginDependencyList } from "@components/settings/tabs/plugins";
import { PluginCard } from "@components/settings/tabs/plugins/PluginCard";
import { ChangeList } from "@utils/ChangeList";
import { classNameFactory } from "@utils/css";
import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { CloseButton, closeModal, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { useForceUpdater } from "@utils/react";
import { findComponentByCodeLazy } from "@webpack";
import { Tooltip, useMemo } from "@webpack/common";
@ -27,7 +27,6 @@ import { getNewPlugins, getNewSettings, KnownPluginSettingsMap, writeKnownSettin
const cl = classNameFactory("vc-new-plugins-");
const CloseButton = findComponentByCodeLazy("CLOSE_BUTTON_LABEL");
const Checkbox = findComponentByCodeLazy('"data-toggleable-component":"checkbox');
let hasSeen = false;

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { filters, findByCodeLazy, mapMangledModuleLazy } from "@webpack";
import { filters, findByCodeLazy, findComponentByCodeLazy, mapMangledModuleLazy } from "@webpack";
import type { ComponentType, PropsWithChildren, ReactNode, Ref } from "react";
import { LazyComponent } from "./react";
@ -114,6 +114,7 @@ export const ModalHeader = LazyComponent(() => Modals.ModalHeader);
export const ModalContent = LazyComponent(() => Modals.ModalContent);
export const ModalFooter = LazyComponent(() => Modals.ModalFooter);
export const ModalCloseButton = LazyComponent(() => Modals.ModalCloseButton);
export const CloseButton = findComponentByCodeLazy("CLOSE_BUTTON_LABEL");
export type MediaModalItem = {
url: string;