mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-25 22:37:02 -04:00
forked!!
This commit is contained in:
parent
538b87062a
commit
ea7451bcdc
326 changed files with 24876 additions and 2280 deletions
213
src/equicordplugins/holyNotes/NoteHandler.ts
Normal file
213
src/equicordplugins/holyNotes/NoteHandler.ts
Normal file
|
@ -0,0 +1,213 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { findByCode } from "@webpack";
|
||||
import { ChannelStore, lodash, Toasts, UserStore } from "@webpack/common";
|
||||
import { Channel, Message } from "discord-types/general";
|
||||
|
||||
import { Discord, HolyNotes } from "./types";
|
||||
import { deleteCacheFromDataStore, DeleteEntireStore, saveCacheToDataStore } from "./utils";
|
||||
|
||||
export const noteHandlerCache = new Map();
|
||||
|
||||
export default new (class NoteHandler {
|
||||
private _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]'.
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||
timestamp: message.timestamp.toString(),
|
||||
attachments: message.attachments as Discord.Attachment[],
|
||||
embeds: message.embeds,
|
||||
reactions: message.reactions as Discord.Reaction[],
|
||||
stickerItems: message.stickerItems,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public getNotes(notebook?: string): Record<string, HolyNotes.Note> {
|
||||
return noteHandlerCache.get(notebook);
|
||||
}
|
||||
|
||||
public getAllNotes(): HolyNotes.Note[] {
|
||||
const data = noteHandlerCache.entries();
|
||||
const notes = {};
|
||||
for (const [key, value] of data) {
|
||||
notes[key] = value;
|
||||
}
|
||||
return notes as HolyNotes.Note[];
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (notebookExists) {
|
||||
Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
message: `Notebook ${notebookName} already exists.`,
|
||||
type: Toasts.Type.FAILURE,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
noteHandlerCache.set(notebookName, {});
|
||||
saveCacheToDataStore(notebookName, {} as HolyNotes.Note[]);
|
||||
|
||||
if (!silent) return Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
message: `Successfully created ${notebookName}.`,
|
||||
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 deleteNotebook = async (notebookName: string) => {
|
||||
noteHandlerCache.delete(notebookName);
|
||||
deleteCacheFromDataStore(notebookName);
|
||||
|
||||
Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
message: `Successfully deleted ${notebookName}.`,
|
||||
type: Toasts.Type.SUCCESS,
|
||||
});
|
||||
};
|
||||
|
||||
public refreshAvatars = async () => {
|
||||
const notebooks = this.getAllNotes();
|
||||
|
||||
const User = findByCode("tag", "isClyde");
|
||||
|
||||
|
||||
|
||||
for (const notebook in notebooks)
|
||||
for (const noteId in notebooks[notebook]) {
|
||||
const note = notebooks[notebook][noteId];
|
||||
const user = UserStore.getUser(note.author.id) ?? new User({ ...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 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,
|
||||
});
|
||||
|
||||
};
|
||||
});
|
23
src/equicordplugins/holyNotes/components/icons/HelpIcon.tsx
Normal file
23
src/equicordplugins/holyNotes/components/icons/HelpIcon.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { classes } from "@utils/misc";
|
||||
|
||||
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>
|
||||
);
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
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;
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
const OverFlowIcon = ({ width = 24, height = 24, color = "var(--interactive-normal)", 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;
|
51
src/equicordplugins/holyNotes/components/modals/Error.tsx
Normal file
51
src/equicordplugins/holyNotes/components/modals/Error.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { findByProps } from "@webpack";
|
||||
|
||||
|
||||
export default ({ error }: { error?: Error; } = {}) => {
|
||||
const classes = findByProps("emptyResultsWrap");
|
||||
|
||||
if (error) {
|
||||
// Error
|
||||
console.log(error);
|
||||
return (
|
||||
<div className={classes.emptyResultsWrap}>
|
||||
<div className={classes.emptyResultsContent} style={{ paddingBottom: "0px" }}>
|
||||
<div className={classes.errorImage} />
|
||||
<div className={classes.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!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (Math.floor(Math.random() * 100) <= 10)
|
||||
// Easter egg
|
||||
return (
|
||||
<div className={classes.emptyResultsWrap}>
|
||||
<div className={classes.emptyResultsContent} style={{ paddingBottom: "0px" }}>
|
||||
<div className={`${classes.noResultsImage} ${classes.alt}`} />
|
||||
<div className={classes.emptyResultsText}>
|
||||
No notes were found. Empathy banana is here for you.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
// Empty notebook
|
||||
else
|
||||
return (
|
||||
<div className={classes.emptyResultsWrap}>
|
||||
<div className={classes.emptyResultsContent} style={{ paddingBottom: "0px" }}>
|
||||
<div className={classes.noResultsImage} />
|
||||
<div className={classes.emptyResultsText}>
|
||||
No notes were found saved in this notebook.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
||||
import { findByProps } from "@webpack";
|
||||
import { Button, Forms, Text } from "@webpack/common";
|
||||
|
||||
import noteHandler from "../../NoteHandler";
|
||||
import { downloadNotes, uploadNotes } from "../../utils";
|
||||
|
||||
export default ({ onClose, ...modalProps }: ModalProps & { onClose: () => void; }) => {
|
||||
const { colorStatusGreen } = findByProps("colorStatusGreen");
|
||||
|
||||
return (
|
||||
<ModalRoot {...modalProps} className="vc-help-modal" size={ModalSize.MEDIUM}>
|
||||
<ModalHeader className="notebook-header">
|
||||
<Text tag="h3">Help Modal</Text>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<div className="vc-help-markdown">
|
||||
<Text>Adding Notes</Text>
|
||||
<Forms.FormText>
|
||||
To add a note right click on a message then hover over the "Note Message" item and click
|
||||
<br />
|
||||
the button with the notebook name you would like to note the message to.
|
||||
<br />
|
||||
<span style={{ fontWeight: "bold" }} className={colorStatusGreen}>
|
||||
Protip:
|
||||
</span>{" "}
|
||||
Clicking the "Note Message" button by itself will note to Main by default!
|
||||
</Forms.FormText>
|
||||
<hr />
|
||||
<Text>Deleting Notes</Text>
|
||||
<Forms.FormText>
|
||||
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!
|
||||
</Forms.FormText>
|
||||
<hr />
|
||||
<Text>Moving Notes</Text>
|
||||
<Forms.FormText>
|
||||
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.
|
||||
</Forms.FormText>
|
||||
<hr />
|
||||
<Text>Jump To Message</Text>
|
||||
<Forms.FormText>
|
||||
To jump to the location that the note was originally located at just right click on the
|
||||
note and hit "Jump to Message".
|
||||
</Forms.FormText>
|
||||
</div>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<div className="vc-notebook-display-left">
|
||||
<Button
|
||||
look={Button.Looks.FILLED}
|
||||
color={Button.Colors.GREEN}
|
||||
style={{ marginRight: "10px" }}
|
||||
onClick={() => {
|
||||
noteHandler.refreshAvatars();
|
||||
}}>Refresh Avatars</Button>
|
||||
<Button
|
||||
look={Button.Looks.FILLED}
|
||||
color={Button.Colors.GREEN}
|
||||
style={{ marginRight: "10px" }}
|
||||
onClick={() => {
|
||||
uploadNotes();
|
||||
}}>Import Notes</Button>
|
||||
<Button
|
||||
look={Button.Looks.FILLED}
|
||||
color={Button.Colors.GREEN}
|
||||
style={{ marginRight: "70px" }}
|
||||
onClick={() => {
|
||||
downloadNotes();
|
||||
}}>Export Notes</Button>
|
||||
<Button
|
||||
look={Button.Looks.FILLED}
|
||||
color={Button.Colors.RED}
|
||||
onClick={() => {
|
||||
noteHandler.deleteEverything();
|
||||
}}>Delete All Notes</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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>
|
||||
);
|
||||
};
|
185
src/equicordplugins/holyNotes/components/modals/NoteBookTab.tsx
Normal file
185
src/equicordplugins/holyNotes/components/modals/NoteBookTab.tsx
Normal file
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* 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 { findByProps } from "@webpack";
|
||||
import { Button, Clickable, Menu, Popout, React } from "@webpack/common";
|
||||
|
||||
import { SvgOverFlowIcon } from "../icons/overFlowIcon";
|
||||
|
||||
|
||||
|
||||
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);
|
||||
|
||||
const { isNotNullish } = findByProps("isNotNullish");
|
||||
const { overflowIcon } = findByProps("overflowIcon");
|
||||
|
||||
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]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes("vc-notebook-tabbar")}
|
||||
ref={tabBarRef}
|
||||
>
|
||||
|
||||
{tabs.map(tab => {
|
||||
if (!overflowedTabs.includes(tab)) {
|
||||
return (
|
||||
<TabItem
|
||||
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}
|
||||
>
|
||||
{props => (
|
||||
<Button
|
||||
{...props}
|
||||
className={"vc-notebook-overflow-chevron"}
|
||||
size={Button.Sizes.ICON}
|
||||
look={Button.Looks.BLANK}
|
||||
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
|
||||
};
|
||||
}
|
174
src/equicordplugins/holyNotes/components/modals/Notebook.tsx
Normal file
174
src/equicordplugins/holyNotes/components/modals/Notebook.tsx
Normal file
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { classes } from "@utils/misc";
|
||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { findByProps } from "@webpack";
|
||||
import { ContextMenuApi, Flex, FluxDispatcher, Menu, React, Text, TextInput } from "@webpack/common";
|
||||
|
||||
import noteHandler from "../../NoteHandler";
|
||||
import { HolyNotes } from "../../types";
|
||||
import HelpIcon from "../icons/HelpIcon";
|
||||
import Errors from "./Error";
|
||||
import HelpModal from "./HelpModal";
|
||||
import ManageNotebookButton from "./ManageNotebookButton";
|
||||
import { CreateTabBar } from "./NoteBookTab";
|
||||
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;
|
||||
}) => {
|
||||
const messageArray = Object.values(notes).map(note => (
|
||||
<RenderMessage
|
||||
note={note}
|
||||
notebook={notebook}
|
||||
updateParent={updateParent}
|
||||
fromDeleteModal={false}
|
||||
closeModal={closeModal}
|
||||
/>
|
||||
));
|
||||
|
||||
if (sortType)
|
||||
messageArray.sort(
|
||||
(a, b) =>
|
||||
new Date(b.props.note?.timestamp)?.getTime() - new Date(a.props.note?.timestamp)?.getTime(),
|
||||
);
|
||||
|
||||
if (sortDirection) messageArray.reverse();
|
||||
|
||||
const filteredMessages = messageArray.filter(message =>
|
||||
message.props.note?.content?.toLowerCase().includes(searchInput.toLowerCase()),
|
||||
);
|
||||
|
||||
return filteredMessages.length > 0 ? filteredMessages : <Errors />;
|
||||
};
|
||||
|
||||
|
||||
|
||||
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 { quickSelect, quickSelectLabel, quickSelectQuick, quickSelectValue, quickSelectArrow } = findByProps("quickSelect");
|
||||
|
||||
const forceUpdate = React.useReducer(() => ({}), {})[1] as () => void;
|
||||
|
||||
const notes = noteHandler.getNotes(currentNotebook);
|
||||
if (!notes) return <></>;
|
||||
|
||||
const { TabBar, selectedTab } = CreateTabBar({ tabs: noteHandler.getAllNotes(), firstSelectedTab: currentNotebook, onChangeTab: setCurrentNotebook });
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<ModalRoot {...props} className={classes("vc-notebook")} size={ModalSize.LARGE}>
|
||||
<Flex className={classes("vc-notebook-flex")} direction={Flex.Direction.VERTICAL} style={{ width: "100%" }}>
|
||||
<div className={classes("vc-notebook-top-section")}>
|
||||
<ModalHeader className={classes("vc-notebook-header-main")}>
|
||||
<Text
|
||||
variant="heading-lg/semibold"
|
||||
style={{ flexGrow: 1 }}
|
||||
className={classes("vc-notebook-heading")}>
|
||||
NOTEBOOK
|
||||
</Text>
|
||||
<div className={classes("vc-notebook-flex", "vc-help-icon")} onClick={() => openModal(HelpModal)}>
|
||||
<HelpIcon />
|
||||
</div>
|
||||
<div style={{ marginBottom: "10px" }} className={classes("vc-notebook-search")}>
|
||||
<TextInput
|
||||
autoFocus={false}
|
||||
placeholder="Search for a message..."
|
||||
onChange={e => setSearch(e)}
|
||||
/>
|
||||
</div>
|
||||
<ModalCloseButton onClick={props.onClose} />
|
||||
</ModalHeader>
|
||||
<div className={classes("vc-notebook-tabbar-container")}>
|
||||
{TabBar}
|
||||
</div>
|
||||
</div>
|
||||
<ModalContent style={{ marginTop: "20px" }}>
|
||||
<ErrorBoundary>
|
||||
{renderNotebook({
|
||||
notes,
|
||||
notebook: currentNotebook,
|
||||
updateParent: () => forceUpdate(),
|
||||
sortDirection: sortDirection,
|
||||
sortType: sortType,
|
||||
searchInput: searchInput,
|
||||
closeModal: props.onClose,
|
||||
})}
|
||||
</ErrorBoundary>
|
||||
</ModalContent>
|
||||
</Flex>
|
||||
<ModalFooter>
|
||||
<ManageNotebookButton notebook={currentNotebook} setNotebook={setCurrentNotebook} />
|
||||
<div className={classes("sort-button-container", "vc-notebook-display-left")}>
|
||||
<Flex
|
||||
align={Flex.Align.CENTER}
|
||||
className={quickSelect}
|
||||
onClick={(event: React.MouseEvent<HTMLDivElement>) => {
|
||||
ContextMenuApi.openContextMenu(event, () => (
|
||||
<Menu.Menu
|
||||
navId="sort-menu"
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
aria-label="Sort Menu"
|
||||
>
|
||||
<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>
|
||||
|
||||
));
|
||||
}}
|
||||
>
|
||||
<Text className={quickSelectLabel}>Change Sorting:</Text>
|
||||
<Flex grow={0} align={Flex.Align.CENTER} className={quickSelectQuick}>
|
||||
<Text className={quickSelectValue}>
|
||||
{sortDirection ? "Ascending" : "Descending"} /{" "}
|
||||
{sortType ? "Date Added" : "Message Date"}
|
||||
</Text>
|
||||
<div className={quickSelectArrow} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
||||
import { Button, React, Text, TextInput } from "@webpack/common";
|
||||
|
||||
import noteHandler from "../../NoteHandler";
|
||||
|
||||
export default (props: ModalProps & { onClose: () => void; }) => {
|
||||
const [notebookName, setNotebookName] = React.useState("");
|
||||
|
||||
const handleCreateNotebook = React.useCallback(() => {
|
||||
if (notebookName !== "") noteHandler.newNoteBook(notebookName);
|
||||
props.onClose();
|
||||
}, [notebookName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ModalRoot className="vc-create-notebook" size={ModalSize.SMALL} {...props}>
|
||||
<ModalHeader className="vc-notebook-header">
|
||||
<Text tag="h3">Create Notebook</Text>
|
||||
<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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
||||
import { Button, React, Text } from "@webpack/common";
|
||||
|
||||
import noteHandler from "../../NoteHandler";
|
||||
import Error from "./Error";
|
||||
import { RenderMessage } from "./RenderMessage";
|
||||
|
||||
export default ({ onClose, notebook, onChangeTab, ...props }: ModalProps & { onClose: () => void; notebook: string; onChangeTab: React.Dispatch<React.SetStateAction<string>>; }) => {
|
||||
const notes = noteHandler.getNotes(notebook);
|
||||
|
||||
const handleDelete = () => {
|
||||
onClose();
|
||||
onChangeTab("Main");
|
||||
noteHandler.deleteNotebook(notebook);
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalRoot
|
||||
{...props}
|
||||
className="vc-delete-notebook"
|
||||
size={ModalSize.LARGE}>
|
||||
<ModalHeader>
|
||||
<Text tag="h3">Confirm Deletion</Text>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<ErrorBoundary>
|
||||
{notes && Object.keys(notes).length > 0 ? (
|
||||
Object.values(notes).map(note => (
|
||||
<RenderMessage
|
||||
note={note}
|
||||
notebook={notebook}
|
||||
fromDeleteModal={true} />
|
||||
))
|
||||
) : (
|
||||
<Error />
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color={Button.Colors.RED}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
DELETE
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* 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 { ModalProps } from "@utils/modal";
|
||||
import { findByCode, findByProps } from "@webpack";
|
||||
import { Clipboard, ContextMenuApi, FluxDispatcher, Menu, NavigationRouter, React } from "@webpack/common";
|
||||
|
||||
import noteHandler from "../../NoteHandler";
|
||||
import { HolyNotes } from "../../types";
|
||||
|
||||
|
||||
export const RenderMessage = ({
|
||||
note,
|
||||
notebook,
|
||||
updateParent,
|
||||
fromDeleteModal,
|
||||
closeModal,
|
||||
}: {
|
||||
note: HolyNotes.Note;
|
||||
notebook: string;
|
||||
updateParent?: () => void;
|
||||
fromDeleteModal: boolean;
|
||||
closeModal?: () => void;
|
||||
}) => {
|
||||
const ChannelMessage = findByProps("ThreadStarterChatMessage").default;
|
||||
const { message, groupStart, cozyMessage } = findByProps("cozyMessage");
|
||||
const User = findByCode("isClyde(){");
|
||||
const Message = findByCode("isEdited(){");
|
||||
const Channel = findByProps("ChannelRecordBase").ChannelRecordBase;
|
||||
|
||||
const [isHoldingDelete, setHoldingDelete] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const deleteHandler = (e: { key: string; type: string; }) =>
|
||||
e.key.toLowerCase() === "delete" && setHoldingDelete(e.type.toLowerCase() === "keydown");
|
||||
|
||||
document.addEventListener("keydown", deleteHandler);
|
||||
document.addEventListener("keyup", deleteHandler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", deleteHandler);
|
||||
document.removeEventListener("keyup", deleteHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="vc-holy-note"
|
||||
style={{
|
||||
marginBottom: "8px",
|
||||
marginTop: "8px",
|
||||
paddingTop: "4px",
|
||||
paddingBottom: "4px",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isHoldingDelete && !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}
|
||||
/>
|
||||
));
|
||||
}}
|
||||
>
|
||||
<ChannelMessage
|
||||
className={classes("vc-holy-render", message, groupStart, cozyMessage)}
|
||||
key={note.id}
|
||||
groupId={note.id}
|
||||
id={note.id}
|
||||
compact={false}
|
||||
isHighlight={false}
|
||||
isLastItem={false}
|
||||
renderContentOnly={false}
|
||||
// @ts-ignore
|
||||
channel={new Channel({ id: "holy-notes" })}
|
||||
message={
|
||||
new Message(
|
||||
Object.assign(
|
||||
{ ...note },
|
||||
{
|
||||
author: new User({ ...note?.author }),
|
||||
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,
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NoteContextMenu = (
|
||||
props: ModalProps & {
|
||||
updateParent?: () => void;
|
||||
notebook: string;
|
||||
note: HolyNotes.Note;
|
||||
closeModal?: () => void;
|
||||
}) => {
|
||||
const { note, notebook, updateParent, closeModal } = props;
|
||||
|
||||
return (
|
||||
<Menu.Menu
|
||||
navId="holynotes"
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
aria-label="Holy Notes"
|
||||
>
|
||||
<Menu.MenuItem
|
||||
label="Jump To Message"
|
||||
id="jump"
|
||||
action={() => {
|
||||
NavigationRouter.transitionTo(`/channels/${note.guild_id ?? "@me"}/${note.channel_id}/${note.id}`);
|
||||
closeModal?.();
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
label="Copy Text"
|
||||
id="copy-text"
|
||||
action={() => Clipboard.copy(note.content)}
|
||||
/>
|
||||
{note?.attachments.length ? (
|
||||
<Menu.MenuItem
|
||||
label="Copy Attachment URL"
|
||||
id="copy-url"
|
||||
action={() => Clipboard.copy(note.attachments[0].url)}
|
||||
/>) : null}
|
||||
<Menu.MenuItem
|
||||
color="danger"
|
||||
label="Delete Note"
|
||||
id="delete"
|
||||
action={() => {
|
||||
noteHandler.deleteNote(note.id, notebook);
|
||||
updateParent?.();
|
||||
}}
|
||||
/>
|
||||
{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
|
||||
label={`Move to ${key}`}
|
||||
id={key}
|
||||
action={() => {
|
||||
noteHandler.moveNote(note, notebook, key);
|
||||
updateParent?.();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Menu.MenuItem>
|
||||
) : null}
|
||||
<Menu.MenuItem
|
||||
label="Copy ID"
|
||||
id="copy-id"
|
||||
action={() => Clipboard.copy(note.id)}
|
||||
/>
|
||||
</Menu.Menu>
|
||||
);
|
||||
|
||||
};
|
132
src/equicordplugins/holyNotes/index.tsx
Normal file
132
src/equicordplugins/holyNotes/index.tsx
Normal file
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./style.css";
|
||||
|
||||
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { DataStore } from "@api/index";
|
||||
import { addButton, removeButton } from "@api/MessagePopover";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { classes } from "@utils/misc";
|
||||
import { openModal } from "@utils/modal";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByProps, findExportedComponentLazy } from "@webpack";
|
||||
import { ChannelStore, Menu } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
||||
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";
|
||||
|
||||
const HeaderBarIcon = findExportedComponentLazy("Icon", "Divider");
|
||||
|
||||
const messageContextMenuPatch: NavContextMenuPatchCallback = async (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={notebook}
|
||||
id={notebook}
|
||||
action={() => noteHandler.addNote(message, notebook)}
|
||||
/>
|
||||
))}
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
function ToolBarHeader() {
|
||||
const iconClasses = findByProps("iconWrapper", "clickable");
|
||||
|
||||
return (
|
||||
<ErrorBoundary noop={true}>
|
||||
<HeaderBarIcon
|
||||
tooltip="Holy Notes"
|
||||
position="bottom"
|
||||
className={classes("vc-note-button", iconClasses.iconWrapper, iconClasses.clickable)}
|
||||
icon={e => Popover(e)}
|
||||
onClick={() => openModal(props => <NoteModal {...props} />)}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default definePlugin({
|
||||
name: "HolyNotes",
|
||||
description: "Holy Notes allows you to save messages",
|
||||
authors: [Devs.Wolfie],
|
||||
dependencies: ["MessagePopoverAPI", "ChatInputButtonAPI"],
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "toolbar:function",
|
||||
replacement: {
|
||||
match: /(function \i\(\i\){)(.{1,200}toolbar.{1,100}mobileToolbar)/,
|
||||
replace: "$1$self.toolbarAction(arguments[0]);$2"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
toolboxActions: {
|
||||
async "Open Notes"() {
|
||||
openModal(props => <NoteModal {...props} />);
|
||||
}
|
||||
},
|
||||
|
||||
contextMenus: {
|
||||
"message": messageContextMenuPatch
|
||||
},
|
||||
|
||||
toolbarAction(e) {
|
||||
if (Array.isArray(e.toolbar))
|
||||
return e.toolbar.push(
|
||||
<ErrorBoundary noop={true}>
|
||||
<ToolBarHeader />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
e.toolbar = [
|
||||
<ErrorBoundary noop={true}>
|
||||
<ToolBarHeader />
|
||||
</ErrorBoundary>,
|
||||
e.toolbar,
|
||||
];
|
||||
},
|
||||
async start() {
|
||||
if (await DataStore.keys(HolyNoteStore).then(keys => !keys.includes("Main"))) return noteHandler.newNoteBook("Main");
|
||||
if (!noteHandlerCache.has("Main")) await DataStoreToCache();
|
||||
|
||||
addButton("HolyNotes", message => {
|
||||
return {
|
||||
label: "Save Note",
|
||||
icon: NoteButtonPopover,
|
||||
message: message,
|
||||
channel: ChannelStore.getChannel(message.channel_id),
|
||||
onClick: () => noteHandler.addNote(message, "Main")
|
||||
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
async stop() {
|
||||
removeButton("HolyNotes");
|
||||
}
|
||||
});
|
||||
|
136
src/equicordplugins/holyNotes/style.css
Normal file
136
src/equicordplugins/holyNotes/style.css
Normal file
|
@ -0,0 +1,136 @@
|
|||
/* stylelint-disable shorthand-property-no-redundant-values */
|
||||
/* stylelint-disable length-zero-no-unit */
|
||||
|
||||
/*
|
||||
$modifier: var(--background-modifier-accent);
|
||||
*/
|
||||
|
||||
.vc-notebook-search {
|
||||
flex: auto;
|
||||
margin-right: 15px;
|
||||
margin-bottom: 6px;
|
||||
background-color: var(--background-tertiary);
|
||||
border: solid 2px var(--background-secondary);
|
||||
}
|
||||
|
||||
.vc-notebook-header {
|
||||
margin-bottom: 10px;
|
||||
padding: 16px 16px 10px 16px;
|
||||
}
|
||||
|
||||
.vc-notebook-header-main {
|
||||
padding-top: 15px;
|
||||
padding-left: 10px;
|
||||
padding-bottom: 0px;
|
||||
background-color: var(--background-tertiary);
|
||||
box-shadow: 0 1px 0 0 var(--background-tertiary), 0 1px 2px 0 var(--background-tertiary) !important;
|
||||
}
|
||||
|
||||
.vc-notebook-heading {
|
||||
max-width: 110px;
|
||||
margin-right: 0px;
|
||||
padding-bottom: 6px;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
|
||||
.vc-notebook-flex {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vc-notebook-flex .vc-help-icon {
|
||||
width: 23px;
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
padding-left: 0px;
|
||||
margin: 0px 14px 6px 0px;
|
||||
color: var(--interactive-normal);
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.vc-notebook-flex .help-icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.vc-notebook-display-left {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.vc-help-markdown {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.vc-help-markdown hr {
|
||||
border: 0;
|
||||
height: 2px;
|
||||
margin: 12px 0px 16px 0px;
|
||||
background-color: var(--background-modifier-accent);
|
||||
}
|
||||
|
||||
.vc-holy-note [class*="buttonContainer"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vc-holy-note [class*="messageListItem"] {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.vc-notebook-tabbar {
|
||||
border-bottom: 1px solid var(--background-tertiary);
|
||||
background-color: var(--background-tertiary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
height: 36px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
/* .vc-notebook-tabbar-bar {
|
||||
margin: 0;
|
||||
padding: 10px 20px 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(--profile-body-divider-color);
|
||||
margin: 20px 12px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.vc-notebook-tabbar-item {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
line-height: 22px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
color: var(--interactive-normal);
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-style: solid;
|
||||
/* stylelint-disable-next-line declaration-block-no-redundant-longhand-properties */
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
.vc-notebook-selected {
|
||||
border-bottom-color: var(--control-brand-foreground);
|
||||
}
|
||||
|
||||
.vc-notebook-tabbar-item:hover {
|
||||
border-bottom-color: var(--brand-experiment);
|
||||
}
|
||||
|
||||
.vc-notebook-top-section {
|
||||
margin-bottom: calc(-8px + .15*(var(--custom-user-profile-modal-header-avatar-size) + var(--custom-user-profile-modal-header-total-avatar-border-size)));
|
||||
z-index: 1;
|
||||
}
|
47
src/equicordplugins/holyNotes/types.ts
Normal file
47
src/equicordplugins/holyNotes/types.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Embed, MessageAttachment, MessageReaction } from "discord-types/general";
|
||||
|
||||
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 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[];
|
||||
}
|
||||
}
|
106
src/equicordplugins/holyNotes/utils.ts
Normal file
106
src/equicordplugins/holyNotes/utils.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { createStore } from "@api/DataStore";
|
||||
import { DataStore } from "@api/index";
|
||||
import { Toasts } from "@webpack/common";
|
||||
|
||||
import noteHandler, { noteHandlerCache } from "./NoteHandler";
|
||||
import { HolyNotes } from "./types";
|
||||
|
||||
export const HolyNoteStore = createStore("HolyNoteData", "HolyNoteStore");
|
||||
|
||||
export async function saveCacheToDataStore(key: string, value?: HolyNotes.Note[]) {
|
||||
await 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 async function DataStoreToCache() {
|
||||
const data = await DataStore.entries(HolyNoteStore);
|
||||
|
||||
data.forEach(function (note) {
|
||||
noteHandlerCache.set(note[0].toString(), note[1]);
|
||||
});
|
||||
}
|
||||
|
||||
export async function DeleteEntireStore() {
|
||||
await DataStore.clear(HolyNoteStore);
|
||||
return noteHandler.newNoteBook("Main", true);
|
||||
}
|
||||
|
||||
export async function downloadNotes() {
|
||||
const filename = "notes.json";
|
||||
const exportData = await noteHandler.exportNotes();
|
||||
const data = JSON.stringify(exportData, null, 2);
|
||||
|
||||
if (IS_VESKTOP || 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(() => {
|
||||
URL.revokeObjectURL(a.href);
|
||||
document.body.removeChild(a);
|
||||
});
|
||||
} else {
|
||||
DiscordNative.fileManager.saveWithDialog(data, filename);
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadNotes() {
|
||||
if (IS_VESKTOP || IS_WEB) {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.style.display = "none";
|
||||
input.accept = "application/json";
|
||||
input.onchange = async () => {
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
try {
|
||||
await noteHandler.importNotes(reader.result as unknown as HolyNotes.Note[]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
message: "Invalid JSON.",
|
||||
type: Toasts.Type.FAILURE,
|
||||
});
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
document.body.appendChild(input);
|
||||
input.click();
|
||||
setImmediate(() => document.body.removeChild(input));
|
||||
} else {
|
||||
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[]);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue