fixes (#705)
* 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:
parent
2ae1062a28
commit
7d532a3bfb
27 changed files with 697 additions and 1500 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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");
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue