Feat/channeltabs (#435)
* add serialization for settings and a myraid of other fixes rewrites. * test dummy settings * fix issue with commits not being logged, rewrite some elements * remove dummy setting * windows titlebar padding fix (maybe works maybe not) * scaling, padding with linux, global rendering * inverse padding logic * disable compact expand on hover setting * myriad of fixes, enhanced bookmark system, and shit * change to intl * fix crashing * fix errorboundry * fixes * whitespace moment * whitespace moment * icon fixes * Polish bookmark navigation * resize handle settings * lint horror * proper message requests support * Bookmark special pages support (Quests, Shop, Message Requests, Discovery) * nitro page support (with a lil easter egg) * active quest gradient (toggleable) * different icon method --------- Co-authored-by: Jake <jake@Georges-MacBook-Air.local> Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
parent
2c811c791d
commit
079f462e2e
10 changed files with 1157 additions and 47 deletions
|
|
@ -9,9 +9,10 @@ import { getGuildAcronym, getIntlMessage } from "@utils/discord";
|
|||
import { classes } from "@utils/misc";
|
||||
import { closeModal, openModal } from "@utils/modal";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { Avatar, ChannelStore, ContextMenuApi, FluxDispatcher, GuildStore, Menu, ReadStateStore, ReadStateUtils, Text, Tooltip, useDrag, useDrop, useEffect, useRef, UserStore } from "@webpack/common";
|
||||
import { Avatar, ChannelStore, ContextMenuApi, FluxDispatcher, GuildStore, Menu, React, ReadStateStore, ReadStateUtils, SelectedChannelStore, SelectedGuildStore, Text, Tooltip, useDrag, useDrop, useEffect, useRef, UserStore, useState } from "@webpack/common";
|
||||
|
||||
import { BasicChannelTabsProps, Bookmark, BookmarkFolder, BookmarkProps, CircleQuestionIcon, isBookmarkFolder, settings, switchChannel, useBookmarks } from "../util";
|
||||
import { BasicChannelTabsProps, Bookmark, BookmarkFolder, BookmarkProps, isBookmarkFolder, isTabSelected, navigateToBookmark, openedTabs, settings, switchChannel, useBookmarks } from "../util";
|
||||
import { CircleQuestionIcon, DiscoveryIcon, EnvelopeIcon, FriendsIcon, NitroIcon, QuestIcon, ShopIcon } from "../util/icons";
|
||||
import { NotificationDot } from "./ChannelTab";
|
||||
import { BookmarkContextMenu, EditModal } from "./ContextMenus";
|
||||
|
||||
|
|
@ -19,6 +20,19 @@ const cl = classNameFactory("vc-channeltabs-");
|
|||
|
||||
const StarIcon = findComponentByCodeLazy(".73-2.25h6.12l1.9-5.83Z");
|
||||
|
||||
function LibraryIcon({ height = 16, width = 16 }: { height?: number; width?: number; }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
height={height}
|
||||
width={width}
|
||||
fill="none"
|
||||
>
|
||||
<path fill="currentColor" d="M3 3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V3zm2 1v16h10V4H5zm13-1h2a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-2V3zm0 2v12h1V5h-1z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function FolderIcon({ fill }: { fill: string; }) {
|
||||
return (
|
||||
<path
|
||||
|
|
@ -39,6 +53,29 @@ function BookmarkIcon({ bookmark }: { bookmark: Bookmark | BookmarkFolder; }) {
|
|||
</svg>
|
||||
);
|
||||
|
||||
// Handle special synthetic pages
|
||||
const { channelId } = bookmark;
|
||||
if (channelId?.startsWith("__")) {
|
||||
const specialIconsMap: Record<string, React.ComponentType<any>> = {
|
||||
"__quests__": QuestIcon,
|
||||
"__message-requests__": EnvelopeIcon,
|
||||
"__friends__": FriendsIcon,
|
||||
"__shop__": ShopIcon,
|
||||
"__library__": () => <LibraryIcon height={16} width={16} />,
|
||||
"__discovery__": DiscoveryIcon,
|
||||
"__nitro__": NitroIcon
|
||||
};
|
||||
|
||||
const IconComponent = specialIconsMap[channelId];
|
||||
if (IconComponent) {
|
||||
// LibraryIcon is a function that returns JSX, others are webpack components
|
||||
if (channelId === "__library__") {
|
||||
return <LibraryIcon height={16} width={16} />;
|
||||
}
|
||||
return <IconComponent height={16} width={16} />;
|
||||
}
|
||||
}
|
||||
|
||||
const channel = ChannelStore.getChannel(bookmark.channelId);
|
||||
const guild = GuildStore.getGuild(bookmark.guildId);
|
||||
if (guild) return guild.icon
|
||||
|
|
@ -167,8 +204,110 @@ function BookmarkFolderOpenMenu(props: BookmarkProps) {
|
|||
);
|
||||
}
|
||||
|
||||
function Bookmark(props: BookmarkProps) {
|
||||
const { bookmarks, index, methods } = props;
|
||||
function FolderBookmarkItem({ bookmark, folderIndex, bookmarkIndex, methods, isCurrentChannel }: {
|
||||
bookmark: Bookmark;
|
||||
folderIndex: number;
|
||||
bookmarkIndex: number;
|
||||
methods: any;
|
||||
isCurrentChannel?: boolean;
|
||||
}) {
|
||||
const { bookmarkNotificationDot } = settings.use(["bookmarkNotificationDot"]);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
// drag implementation for folder items
|
||||
const [, drag] = useDrag(() => ({
|
||||
type: "vc_Bookmark",
|
||||
item: () => ({
|
||||
index: bookmarkIndex, // index within folder
|
||||
folderIndex: folderIndex, // the folder its in
|
||||
bookmark: bookmark,
|
||||
isFromFolder: true
|
||||
}),
|
||||
collect: monitor => ({
|
||||
isDragging: !!monitor.isDragging()
|
||||
}),
|
||||
}), [bookmark, folderIndex, bookmarkIndex]);
|
||||
|
||||
// combine drag ref
|
||||
drag(ref);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={classes(
|
||||
cl("bookmark", "folder-item", "hoverable"),
|
||||
isCurrentChannel && cl("bookmark-active")
|
||||
)}
|
||||
onClick={() => navigateToBookmark(bookmark)}
|
||||
onContextMenu={e => {
|
||||
e.stopPropagation();
|
||||
ContextMenuApi.openContextMenu(e, () => (
|
||||
<Menu.Menu
|
||||
navId="folder-item-context"
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
>
|
||||
<Menu.MenuItem
|
||||
key="edit-bookmark"
|
||||
id="edit-bookmark"
|
||||
label="Edit Bookmark"
|
||||
action={() => {
|
||||
const key = openModal(modalProps =>
|
||||
<EditModal
|
||||
modalProps={modalProps}
|
||||
modalKey={key}
|
||||
bookmark={bookmark}
|
||||
onSave={name => {
|
||||
const folder = methods.bookmarks[folderIndex] as BookmarkFolder;
|
||||
const newBookmarks = [...folder.bookmarks];
|
||||
newBookmarks[bookmarkIndex].name = name;
|
||||
methods.editBookmark(folderIndex, { bookmarks: newBookmarks });
|
||||
closeModal(key);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
key="delete-bookmark"
|
||||
id="delete-bookmark"
|
||||
label="Delete Bookmark"
|
||||
action={() => methods.deleteBookmark(bookmarkIndex, folderIndex)}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
key="remove-from-folder"
|
||||
id="remove-from-folder"
|
||||
label="Remove from Folder"
|
||||
action={() => {
|
||||
methods.addBookmark(bookmark);
|
||||
methods.deleteBookmark(bookmarkIndex, folderIndex);
|
||||
}}
|
||||
/>
|
||||
{bookmarkNotificationDot && ReadStateStore.hasUnread(bookmark.channelId) && (
|
||||
<Menu.MenuItem
|
||||
key="mark-as-read"
|
||||
id="mark-as-read"
|
||||
label={getIntlMessage("MARK_AS_READ")}
|
||||
action={() => ReadStateUtils.ackChannel(ChannelStore.getChannel(bookmark.channelId))}
|
||||
/>
|
||||
)}
|
||||
</Menu.Menu>
|
||||
));
|
||||
}}
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" style={{ marginRight: "0.25rem" }}>
|
||||
<path fill="currentColor" d="M10 17l5-5-5-5z" />
|
||||
</svg>
|
||||
<BookmarkIcon bookmark={bookmark} />
|
||||
<Text variant="text-sm/normal" className={cl("name-text")}>
|
||||
{bookmark.name}
|
||||
</Text>
|
||||
{bookmarkNotificationDot && <NotificationDot channelIds={[bookmark.channelId]} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Bookmark(props: BookmarkProps & { isExpanded?: boolean; onToggleFolder?: () => void; isCurrentChannel?: boolean; }) {
|
||||
const { bookmarks, index, methods, isExpanded, onToggleFolder, isCurrentChannel } = props;
|
||||
const bookmark = bookmarks[index];
|
||||
const { bookmarkNotificationDot } = settings.use(["bookmarkNotificationDot"]);
|
||||
|
||||
|
|
@ -176,13 +315,25 @@ function Bookmark(props: BookmarkProps) {
|
|||
|
||||
const [, drag] = useDrag(() => ({
|
||||
type: "vc_Bookmark",
|
||||
canDrag: () => !isBookmarkFolder(bookmark), // Don't allow dragging folders
|
||||
item: () => {
|
||||
return { index };
|
||||
// find current index dynamically instead of using stale closure
|
||||
const currentIndex = bookmarks.findIndex(b =>
|
||||
isBookmarkFolder(b) ?
|
||||
(isBookmarkFolder(bookmark) && b.bookmarks === bookmark.bookmarks) :
|
||||
(!isBookmarkFolder(bookmark) && b.channelId === bookmark.channelId && b.guildId === bookmark.guildId)
|
||||
);
|
||||
return {
|
||||
index: currentIndex !== -1 ? currentIndex : index,
|
||||
folderIndex: undefined, // not in a folder (bar)
|
||||
bookmark: bookmark, // include full bookmark data
|
||||
isFromFolder: false
|
||||
};
|
||||
},
|
||||
collect: monitor => ({
|
||||
isDragging: !!monitor.isDragging()
|
||||
}),
|
||||
}));
|
||||
}), [bookmarks, bookmark, index]);
|
||||
|
||||
const [, drop] = useDrop(() => ({
|
||||
accept: "vc_Bookmark",
|
||||
|
|
@ -207,21 +358,88 @@ function Bookmark(props: BookmarkProps) {
|
|||
item.index = hoverIndex;
|
||||
},
|
||||
}), []);
|
||||
drag(drop(ref));
|
||||
|
||||
// separate drop zone for folders to accept tabs and bookmarks (prevents duplicates)
|
||||
const [{ isOver: isFolderDropTarget }, folderDrop] = useDrop(() => ({
|
||||
accept: ["vc_ChannelTab", "vc_Bookmark"],
|
||||
canDrop: () => isBookmarkFolder(bookmark),
|
||||
drop: (item: any, monitor) => {
|
||||
if (!isBookmarkFolder(bookmark)) return;
|
||||
|
||||
const itemType = monitor.getItemType();
|
||||
|
||||
// dropping a tab onto a folder
|
||||
if (itemType === "vc_ChannelTab") {
|
||||
methods.addBookmark({
|
||||
guildId: item.guildId,
|
||||
channelId: item.channelId
|
||||
}, index);
|
||||
}
|
||||
|
||||
// dropping a bookmark onto a folder (reorganize)
|
||||
if (itemType === "vc_Bookmark") {
|
||||
const sourceBookmark = item.bookmark || bookmarks[item.index];
|
||||
|
||||
// skip if source is a folder
|
||||
if (isBookmarkFolder(sourceBookmark)) return;
|
||||
|
||||
// skip if already in this folder
|
||||
if (item.isFromFolder && item.folderIndex === index) return;
|
||||
|
||||
// remove from original location
|
||||
if (item.isFromFolder) {
|
||||
// coming from a folder
|
||||
methods.deleteBookmark(item.index, item.folderIndex);
|
||||
} else {
|
||||
// coming from bar level
|
||||
methods.deleteBookmark(item.index);
|
||||
}
|
||||
|
||||
// add to this folder
|
||||
methods.addBookmark(sourceBookmark, index);
|
||||
}
|
||||
},
|
||||
collect: monitor => ({
|
||||
isOver: monitor.isOver({ shallow: true }) && monitor.canDrop()
|
||||
})
|
||||
}), [bookmarks, index, methods]);
|
||||
|
||||
// combine refs differently based on bookmark type
|
||||
if (isBookmarkFolder(bookmark)) {
|
||||
drag(folderDrop(drop(ref)));
|
||||
} else {
|
||||
drag(drop(ref));
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cl("bookmark", "hoverable", { wider: settings.store.widerTabsAndBookmarks })}
|
||||
className={classes(
|
||||
cl("bookmark", "hoverable", { wider: settings.store.widerTabsAndBookmarks }),
|
||||
isFolderDropTarget && cl("folder-drop-target"),
|
||||
isExpanded && cl("folder-expanded"),
|
||||
isCurrentChannel && cl("bookmark-active")
|
||||
)}
|
||||
ref={ref}
|
||||
onClick={e => isBookmarkFolder(bookmark)
|
||||
? ContextMenuApi.openContextMenu(e, () => <BookmarkFolderOpenMenu {...props} />)
|
||||
: switchChannel(bookmark)
|
||||
}
|
||||
onClick={e => {
|
||||
if (isBookmarkFolder(bookmark)) {
|
||||
onToggleFolder?.();
|
||||
} else {
|
||||
navigateToBookmark(bookmark);
|
||||
}
|
||||
}}
|
||||
onContextMenu={e => ContextMenuApi.openContextMenu(e, () =>
|
||||
<BookmarkContextMenu {...props} />
|
||||
)}
|
||||
>
|
||||
<BookmarkIcon bookmark={bookmark} />
|
||||
{isBookmarkFolder(bookmark) && (
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" style={{ marginLeft: "0.25rem" }}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d={isExpanded ? "M7 10l5 5 5-5z" : "M10 17l5-5-5-5z"}
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
<Text variant="text-sm/normal" className={cl("name-text")}>
|
||||
{bookmark.name}
|
||||
</Text>
|
||||
|
|
@ -233,17 +451,30 @@ function Bookmark(props: BookmarkProps) {
|
|||
);
|
||||
}
|
||||
|
||||
function HorizontalScroller({ children, className }: React.PropsWithChildren<{ className?: string; }>) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
function HorizontalScroller({ children, className, customRef }: React.PropsWithChildren<{ className?: string; customRef?: (node: HTMLDivElement) => void; }>) {
|
||||
const internalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
ref.current!.addEventListener("wheel", e => {
|
||||
const element = internalRef.current;
|
||||
if (!element) return;
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
ref.current!.scrollLeft += e.deltaY;
|
||||
});
|
||||
element.scrollLeft += e.deltaY;
|
||||
};
|
||||
|
||||
element.addEventListener("wheel", handleWheel);
|
||||
return () => element.removeEventListener("wheel", handleWheel);
|
||||
}, []);
|
||||
|
||||
// Combine refs
|
||||
const combinedRef = (node: HTMLDivElement) => {
|
||||
internalRef.current = node;
|
||||
if (customRef) customRef(node);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes(cl("scroller"), className)} ref={ref}>
|
||||
<div className={classes(cl("scroller"), className)} ref={combinedRef}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -252,6 +483,31 @@ function HorizontalScroller({ children, className }: React.PropsWithChildren<{ c
|
|||
export default function BookmarkContainer(props: BasicChannelTabsProps & { userId: string; }) {
|
||||
const { guildId, channelId, userId } = props;
|
||||
const [bookmarks, methods] = useBookmarks(userId);
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<number>>(new Set());
|
||||
|
||||
// make setting reactive to changes
|
||||
const { bookmarksIndependentFromTabs } = settings.use(["bookmarksIndependentFromTabs"]);
|
||||
|
||||
const currentChannelId = SelectedChannelStore.getChannelId();
|
||||
const currentGuildId = SelectedGuildStore.getGuildId() || "@me";
|
||||
|
||||
// check if current channel matches the active tab
|
||||
const activeTab = openedTabs.find(tab => isTabSelected(tab.id));
|
||||
const channelMatchesActiveTab = activeTab &&
|
||||
activeTab.channelId === currentChannelId &&
|
||||
(activeTab.guildId || "@me") === currentGuildId;
|
||||
|
||||
const toggleFolder = (folderIndex: number) => {
|
||||
setExpandedFolders(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(folderIndex)) {
|
||||
next.delete(folderIndex);
|
||||
} else {
|
||||
next.add(folderIndex);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
let isCurrentChannelBookmarked = false, currentChannelFolderIndex = -1;
|
||||
bookmarks?.forEach((bookmark, i) => {
|
||||
|
|
@ -264,9 +520,47 @@ export default function BookmarkContainer(props: BasicChannelTabsProps & { userI
|
|||
else if (bookmark.channelId === channelId) isCurrentChannelBookmarked = true;
|
||||
});
|
||||
|
||||
const [{ isOver }, dropRef] = useDrop(() => ({
|
||||
accept: ["vc_ChannelTab", "vc_Bookmark"],
|
||||
drop: (item: any, monitor) => {
|
||||
if (!bookmarks) return;
|
||||
|
||||
// check if a nested drop zone (like a folder) already handled this
|
||||
if (monitor.didDrop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemType = monitor.getItemType();
|
||||
|
||||
// if dropping a tab, create bookmark
|
||||
if (itemType === "vc_ChannelTab") {
|
||||
methods.addBookmark({
|
||||
guildId: item.guildId,
|
||||
channelId: item.channelId
|
||||
});
|
||||
}
|
||||
// if dropping a bookmark, handle based on source
|
||||
else if (itemType === "vc_Bookmark") {
|
||||
if (item.isFromFolder && item.folderIndex !== undefined) {
|
||||
// moving from folder to bar (aka root level)
|
||||
const folder = bookmarks[item.folderIndex];
|
||||
if (isBookmarkFolder(folder)) {
|
||||
const sourceBookmark = item.bookmark || folder.bookmarks[item.index];
|
||||
methods.deleteBookmark(item.index, item.folderIndex);
|
||||
methods.addBookmark(sourceBookmark);
|
||||
}
|
||||
}
|
||||
// if moving from bar to bar, that's just reordering (handled by hover logic)
|
||||
}
|
||||
},
|
||||
collect: monitor => ({
|
||||
isOver: monitor.isOver({ shallow: true })
|
||||
})
|
||||
}), [bookmarks, methods]);
|
||||
|
||||
return (
|
||||
<div className={cl("bookmark-container")}>
|
||||
<HorizontalScroller className={cl("bookmarks")}>
|
||||
<HorizontalScroller className={classes(cl("bookmarks"), isOver && cl("bookmarks-drop-target"))} customRef={dropRef}>
|
||||
{!bookmarks && <Text className={cl("bookmark-placeholder-text")} variant="text-xs/normal">
|
||||
Loading bookmarks...
|
||||
</Text>}
|
||||
|
|
@ -274,9 +568,50 @@ export default function BookmarkContainer(props: BasicChannelTabsProps & { userI
|
|||
You have no bookmarks. You can add an open tab or hide this by right clicking it
|
||||
</Text>}
|
||||
{Array.isArray(bookmarks) && bookmarks.length > 0 &&
|
||||
bookmarks.map((_, i) => (
|
||||
<Bookmark key={i} index={i} bookmarks={bookmarks} methods={methods} />
|
||||
))
|
||||
bookmarks.flatMap((bookmark, i) => {
|
||||
const items = [
|
||||
<Bookmark
|
||||
key={i}
|
||||
index={i}
|
||||
bookmarks={bookmarks}
|
||||
methods={methods}
|
||||
isExpanded={expandedFolders.has(i)}
|
||||
onToggleFolder={() => toggleFolder(i)}
|
||||
isCurrentChannel={
|
||||
bookmarksIndependentFromTabs &&
|
||||
!channelMatchesActiveTab && (
|
||||
!isBookmarkFolder(bookmark) &&
|
||||
bookmark.channelId === currentChannelId &&
|
||||
(bookmark.guildId || "@me") === currentGuildId
|
||||
)
|
||||
}
|
||||
/>
|
||||
];
|
||||
|
||||
// if folder is expanded, render its bookmarks
|
||||
if (isBookmarkFolder(bookmark) && expandedFolders.has(i)) {
|
||||
bookmark.bookmarks.forEach((childBookmark, childIndex) => {
|
||||
items.push(
|
||||
<FolderBookmarkItem
|
||||
key={`${i}-${childIndex}`}
|
||||
bookmark={childBookmark}
|
||||
folderIndex={i}
|
||||
bookmarkIndex={childIndex}
|
||||
methods={methods}
|
||||
isCurrentChannel={
|
||||
bookmarksIndependentFromTabs &&
|
||||
!channelMatchesActiveTab && (
|
||||
childBookmark.channelId === currentChannelId &&
|
||||
(childBookmark.guildId || "@me") === currentGuildId
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
})
|
||||
}
|
||||
</HorizontalScroller>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,21 +5,39 @@
|
|||
*/
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { activeQuestIntervals } from "@equicordplugins/questify"; // sorry murphy!
|
||||
import { getGuildAcronym, getIntlMessage, getUniqueUsername } from "@utils/discord";
|
||||
import { classes } from "@utils/misc";
|
||||
import { Channel, Guild, User } from "@vencord/discord-types";
|
||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { Avatar, ChannelStore, ContextMenuApi, GuildStore, PresenceStore, ReadStateStore, Text, TypingStore, useDrag, useDrop, useEffect, useRef, UserStore, useState, useStateFromStores } from "@webpack/common";
|
||||
import { JSX } from "react";
|
||||
|
||||
import { ChannelTabsProps, CircleQuestionIcon, closeTab, isTabSelected, moveDraggedTabs, moveToTab, openedTabs, settings } from "../util";
|
||||
import { ChannelTabsProps, closeTab, isTabSelected, moveDraggedTabs, moveToTab, openedTabs, settings } from "../util";
|
||||
import { CircleQuestionIcon, DiscoveryIcon, EnvelopeIcon, FriendsIcon, NitroIcon, QuestIcon, ShopIcon } from "../util/icons";
|
||||
import { TabContextMenu } from "./ContextMenus";
|
||||
|
||||
const ThreeDots = findComponentByCodeLazy(".dots,", "dotRadius:");
|
||||
const dotStyles = findByPropsLazy("numberBadge", "textBadge");
|
||||
|
||||
const FriendsIcon = findComponentByCodeLazy("12h1a8");
|
||||
const ChannelTypeIcon = findComponentByCodeLazy(".iconContainerWithGuildIcon,");
|
||||
|
||||
// Custom SVG icons for pages that don't have findable components
|
||||
|
||||
function LibraryIcon(height: number = 20, width: number = 20, className?: string): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
height={height}
|
||||
width={width}
|
||||
fill="none"
|
||||
className={className}
|
||||
>
|
||||
<path fill="currentColor" d="M3 3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V3zm2 1v16h10V4H5zm13-1h2a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-2V3zm0 2v12h1V5h-1z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const cl = classNameFactory("vc-channeltabs-");
|
||||
|
||||
function XIcon({ size, fill }: { size: number, fill: string; }) {
|
||||
|
|
@ -176,13 +194,38 @@ function ChannelTabContent(props: ChannelTabsProps & {
|
|||
}
|
||||
}
|
||||
|
||||
if (guildId === "@me" || guildId === undefined)
|
||||
// handle special synthetic pages
|
||||
if (channelId && channelId.startsWith("__")) {
|
||||
const specialPagesConfig: Record<string, { label: string, Icon: React.ComponentType<any>; }> = {
|
||||
"__quests__": { label: "Quests", Icon: QuestIcon },
|
||||
"__message-requests__": { label: "Message Requests", Icon: EnvelopeIcon },
|
||||
"__friends__": { label: getIntlMessage("FRIENDS"), Icon: FriendsIcon },
|
||||
"__shop__": { label: "Shop", Icon: ShopIcon },
|
||||
"__library__": { label: "Library", Icon: () => LibraryIcon(20, 20) },
|
||||
"__discovery__": { label: "Discovery", Icon: DiscoveryIcon },
|
||||
"__nitro__": { label: "Nitro", Icon: NitroIcon }
|
||||
};
|
||||
|
||||
const pageConfig = specialPagesConfig[channelId];
|
||||
if (pageConfig) {
|
||||
const { label, Icon } = pageConfig;
|
||||
return (
|
||||
<>
|
||||
<Icon />
|
||||
<Text className={cl("name-text")}>{label}</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (guildId === "@me" || guildId === undefined) {
|
||||
return (
|
||||
<>
|
||||
<FriendsIcon />
|
||||
<Text className={cl("name-text")}>{getIntlMessage("FRIENDS")}</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -220,12 +263,52 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
|
|||
const lastSwapTimeRef = useRef(0);
|
||||
const SWAP_THROTTLE_MS = 100;
|
||||
|
||||
const handleResizeStart = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const startX = e.clientX;
|
||||
const startWidth = ref.current?.getBoundingClientRect().width || 0;
|
||||
const baseWidth = 192; // 12rem in pixels (assuming 16px base font)
|
||||
|
||||
document.body.style.cursor = "ew-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
|
||||
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||
const deltaX = moveEvent.clientX - startX;
|
||||
const newWidth = startWidth + deltaX;
|
||||
const newScale = newWidth / baseWidth;
|
||||
|
||||
// 50% and 200% scale
|
||||
const clampedScale = Math.max(0.5, Math.min(2, newScale));
|
||||
settings.store.tabWidthScale = Math.round(clampedScale * 100);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
|
||||
const [, drag] = useDrag(() => ({
|
||||
type: "vc_ChannelTab",
|
||||
item: () => {
|
||||
setIsDragging(true);
|
||||
lastSwapTimeRef.current = Date.now() - SWAP_THROTTLE_MS;
|
||||
return { id };
|
||||
|
||||
// get fresh tab data dynamically to avoid stale closures
|
||||
const tab = openedTabs.find(t => t.id === id);
|
||||
|
||||
return {
|
||||
id,
|
||||
channelId: tab?.channelId || channelId,
|
||||
guildId: tab?.guildId || guildId
|
||||
};
|
||||
},
|
||||
collect: monitor => ({
|
||||
isDragging: !!monitor.isDragging()
|
||||
|
|
@ -235,7 +318,7 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
|
|||
setIsDropTarget(false);
|
||||
lastSwapTimeRef.current = 0;
|
||||
}
|
||||
}));
|
||||
}), [id, channelId, guildId]);
|
||||
const [, drop] = useDrop(() => ({
|
||||
accept: "vc_ChannelTab",
|
||||
hover: (item, monitor) => {
|
||||
|
|
@ -267,7 +350,7 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
|
|||
const hoverWidth = hoverBoundingRect.right - hoverBoundingRect.left;
|
||||
const hoverMiddleX = hoverWidth / 2;
|
||||
|
||||
// Get tab width
|
||||
// get tab width
|
||||
const draggedElement = document.querySelector(".vc-channeltabs-tab-dragging") as HTMLElement;
|
||||
const draggedWidth = draggedElement?.getBoundingClientRect().width || hoverWidth;
|
||||
const halfDraggedWidth = draggedWidth / 2;
|
||||
|
|
@ -295,6 +378,9 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
|
|||
}), []);
|
||||
drag(drop(ref));
|
||||
|
||||
// check if quests running (questify momentLet)
|
||||
const hasActiveQuests = activeQuestIntervals.size > 0;
|
||||
|
||||
return <div
|
||||
className={cl("tab", {
|
||||
"tab-compact": compact,
|
||||
|
|
@ -303,6 +389,8 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
|
|||
"tab-closing": isClosing,
|
||||
"tab-dragging": isDragging,
|
||||
"tab-drop-target": isDropTarget,
|
||||
"tab-nitro": channelId === "__nitro__",
|
||||
"tab-quests-active": channelId === "__quests__" && hasActiveQuests,
|
||||
wider: settings.store.widerTabsAndBookmarks
|
||||
})}
|
||||
key={index}
|
||||
|
|
@ -334,6 +422,11 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
|
|||
>
|
||||
<XIcon size={16} fill="var(--interactive-normal)" />
|
||||
</button>}
|
||||
|
||||
{!compact && settings.store.showResizeHandle && <div
|
||||
className={cl("tab-resize-handle")}
|
||||
onMouseDown={handleResizeStart}
|
||||
/>}
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@
|
|||
*/
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { IS_MAC } from "@utils/constants";
|
||||
import { classes } from "@utils/misc";
|
||||
import { useForceUpdater } from "@utils/react";
|
||||
import { findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Button, ContextMenuApi, Flex, FluxDispatcher, Forms, useCallback, useEffect, useRef, UserStore, useState, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import { BasicChannelTabsProps, ChannelTabsProps, createTab, handleChannelSwitch, moveToTab, openedTabs, openStartupTabs, saveTabs, settings, setUpdaterFunction, useGhostTabs } from "../util";
|
||||
import { BasicChannelTabsProps, ChannelTabsProps, clearStaleNavigationContext, createTab, handleChannelSwitch, isNavigationFromSource, moveToTab, openedTabs, openStartupTabs, saveTabs, settings, setUpdaterFunction, useGhostTabs } from "../util";
|
||||
import BookmarkContainer from "./BookmarkContainer";
|
||||
import ChannelTab, { PreviewTab } from "./ChannelTab";
|
||||
import { BasicContextMenu } from "./ContextMenus";
|
||||
|
|
@ -27,6 +28,7 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
|
|||
const {
|
||||
showBookmarkBar,
|
||||
widerTabsAndBookmarks,
|
||||
tabWidthScale,
|
||||
enableHotkeys,
|
||||
hotkeyCount,
|
||||
tabBarPosition,
|
||||
|
|
@ -42,10 +44,15 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
|
|||
animationSelectedBorder,
|
||||
animationSelectedBackground,
|
||||
animationTabShadows,
|
||||
compactAutoExpandSelected
|
||||
animationTabPositioning,
|
||||
animationResizeHandle,
|
||||
animationQuestsActive,
|
||||
compactAutoExpandSelected,
|
||||
compactAutoExpandOnHover
|
||||
} = settings.use([
|
||||
"showBookmarkBar",
|
||||
"widerTabsAndBookmarks",
|
||||
"tabWidthScale",
|
||||
"enableHotkeys",
|
||||
"hotkeyCount",
|
||||
"tabBarPosition",
|
||||
|
|
@ -61,7 +68,11 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
|
|||
"animationSelectedBorder",
|
||||
"animationSelectedBackground",
|
||||
"animationTabShadows",
|
||||
"compactAutoExpandSelected"
|
||||
"animationTabPositioning",
|
||||
"animationResizeHandle",
|
||||
"animationQuestsActive",
|
||||
"compactAutoExpandSelected",
|
||||
"compactAutoExpandOnHover"
|
||||
]);
|
||||
const GhostTabs = useGhostTabs();
|
||||
const isFullscreen = useStateFromStores([ChannelRTCStore], () => ChannelRTCStore.isFullscreenInContext() ?? false);
|
||||
|
|
@ -135,8 +146,18 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
|
|||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
handleChannelSwitch(props);
|
||||
// Normalize guildId for comparison
|
||||
const normalizedGuildId = props.guildId || "@me";
|
||||
|
||||
// Skip if this navigation came from a bookmark
|
||||
if (!isNavigationFromSource(normalizedGuildId, props.channelId, "bookmark")) {
|
||||
handleChannelSwitch(props);
|
||||
}
|
||||
|
||||
saveTabs(userId);
|
||||
|
||||
// Clean up any stale navigation contexts
|
||||
clearStaleNavigationContext();
|
||||
}
|
||||
}, [userId, props.channelId, props.guildId]);
|
||||
|
||||
|
|
@ -149,6 +170,7 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
|
|||
className={classes(
|
||||
cl("container"),
|
||||
tabBarPosition === "top" && cl("container-top"),
|
||||
IS_MAC && tabBarPosition === "top" && cl("container-top-macos"),
|
||||
!animationHover && cl("no-hover-animation"),
|
||||
!animationSelection && cl("no-selection-animation"),
|
||||
!animationDragDrop && cl("no-drag-animation"),
|
||||
|
|
@ -161,9 +183,14 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
|
|||
!animationSelectedBorder && cl("no-selected-border"),
|
||||
!animationSelectedBackground && cl("no-selected-background"),
|
||||
!animationTabShadows && cl("no-tab-shadows"),
|
||||
!compactAutoExpandSelected && cl("no-compact-auto-expand")
|
||||
!animationTabPositioning && cl("no-tab-positioning"),
|
||||
!animationResizeHandle && cl("no-resize-handle-animation"),
|
||||
!animationQuestsActive && cl("no-quests-active-animation"),
|
||||
!compactAutoExpandSelected && cl("no-compact-auto-expand"),
|
||||
!compactAutoExpandOnHover && cl("no-compact-hover-expand")
|
||||
)}
|
||||
ref={ref}
|
||||
style={{ "--tab-width-scale": tabWidthScale / 100 } as React.CSSProperties}
|
||||
onContextMenu={e => ContextMenuApi.openContextMenu(e, () => <BasicContextMenu />)}
|
||||
>
|
||||
{showBookmarkBar && <>
|
||||
|
|
|
|||
|
|
@ -97,6 +97,63 @@ export default definePlugin({
|
|||
|
||||
containerHeight: 0,
|
||||
|
||||
flux: {
|
||||
CHANNEL_SELECT(data: { channelId: string | null, guildId: string | null; }) {
|
||||
// Skip if this navigation was triggered by us (clicking a tab)
|
||||
if (ChannelTabsUtils.isNavigatingViaTab()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're in bookmark viewing mode
|
||||
const wasViewingViaBookmark = ChannelTabsUtils.isViewingViaBookmarkMode();
|
||||
|
||||
// Clear bookmark viewing mode when ANY navigation occurs
|
||||
// This allows subsequent channel switches to work normally
|
||||
if (wasViewingViaBookmark) {
|
||||
ChannelTabsUtils.clearBookmarkViewingMode();
|
||||
}
|
||||
|
||||
let { channelId } = data;
|
||||
let { guildId } = data;
|
||||
|
||||
// Detect special pages by pathname when no channelId
|
||||
if (!channelId) {
|
||||
const path = window.location.pathname;
|
||||
|
||||
if (path === "/quest-home" || path.includes("quest-home")) {
|
||||
channelId = "__quests__";
|
||||
guildId = "@me";
|
||||
} else if (path.includes("/message-requests")) {
|
||||
channelId = "__message-requests__";
|
||||
guildId = "@me";
|
||||
} else if (path === "/channels/@me") {
|
||||
channelId = "__friends__";
|
||||
guildId = "@me";
|
||||
} else if (path.includes("/shop")) {
|
||||
channelId = "__shop__";
|
||||
guildId = "@me";
|
||||
} else if (path.includes("/library")) {
|
||||
channelId = "__library__";
|
||||
guildId = "@me";
|
||||
} else if (path.includes("/discovery")) {
|
||||
channelId = "__discovery__";
|
||||
guildId = "@me";
|
||||
} else if (path.includes("/store")) {
|
||||
channelId = "__nitro__";
|
||||
guildId = "@me";
|
||||
} else {
|
||||
// Unknown page without channelId - ignore
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, channelId is guaranteed to be non-null
|
||||
if (channelId && guildId) {
|
||||
handleChannelSwitch({ guildId, channelId });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render({ currentChannel, children }: {
|
||||
currentChannel: BasicChannelTabsProps,
|
||||
children: JSX.Element;
|
||||
|
|
@ -134,7 +191,18 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
handleNavigation(guildId: string, channelId: string) {
|
||||
if (!guildId || !channelId) return;
|
||||
// Skip if we initiated this navigation
|
||||
if (ChannelTabsUtils.isNavigatingViaTab()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if we're viewing via bookmarks in independent mode
|
||||
if (ChannelTabsUtils.isViewingViaBookmarkMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear bookmark viewing mode when navigating to a regular channel
|
||||
ChannelTabsUtils.clearBookmarkViewingMode();
|
||||
|
||||
// wait for discord to update channel data
|
||||
requestAnimationFrame(() => {
|
||||
|
|
|
|||
|
|
@ -68,10 +68,14 @@
|
|||
}
|
||||
|
||||
.vc-channeltabs-container-top {
|
||||
padding-left: 80px;
|
||||
padding-left: 4px;
|
||||
box-shadow: inset 0 -1px 0 var(--background-tertiary);
|
||||
}
|
||||
|
||||
.vc-channeltabs-container-top-macos {
|
||||
padding-left: 80px !important;
|
||||
}
|
||||
|
||||
.vc-channeltabs-separator {
|
||||
width: 98%;
|
||||
height: 1px;
|
||||
|
|
@ -94,7 +98,12 @@
|
|||
.vc-channeltabs-bookmarks {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
padding: 2px 0;
|
||||
padding: 2px 0 2px 8px;
|
||||
}
|
||||
|
||||
/* Add spacing between bookmark items */
|
||||
.vc-channeltabs-bookmarks > [class*="scroller"] {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.vc-channeltabs-bookmark-placeholder-text {
|
||||
|
|
@ -121,14 +130,14 @@
|
|||
.vc-channeltabs-bookmark {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 8rem;
|
||||
max-width: calc(8rem * var(--tab-width-scale, 1));
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vc-channeltabs-bookmark.vc-channeltabs-wider {
|
||||
max-width: 12rem;
|
||||
max-width: calc(12rem * var(--tab-width-scale, 1));
|
||||
}
|
||||
|
||||
.vc-channeltabs-bookmark .vc-channeltabs-name-text {
|
||||
|
|
@ -160,7 +169,7 @@
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
width: 12rem;
|
||||
width: calc(12rem * var(--tab-width-scale, 1));
|
||||
min-width: 0;
|
||||
margin: 0 4px;
|
||||
background-color: transparent;
|
||||
|
|
@ -180,8 +189,44 @@
|
|||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.vc-channeltabs-tab-resize-handle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 6px;
|
||||
cursor: ew-resize;
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
background: var(--brand-500);
|
||||
border-radius: 0 11px 11px 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.vc-channeltabs-tab-resize-handle:hover {
|
||||
opacity: 0.7 !important;
|
||||
}
|
||||
|
||||
.vc-channeltabs-no-resize-handle-animation .vc-channeltabs-tab-resize-handle {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.vc-channeltabs-tab:hover .vc-channeltabs-tab-resize-handle {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.vc-channeltabs-tab-resizing {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.vc-channeltabs-tab-resizing * {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.vc-channeltabs-tab.vc-channeltabs-wider:not(.vc-channeltabs-tab-compact) {
|
||||
width: 16rem;
|
||||
width: calc(16rem * var(--tab-width-scale, 1));
|
||||
}
|
||||
|
||||
.vc-channeltabs-tab-compact {
|
||||
|
|
@ -429,7 +474,7 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 12rem;
|
||||
width: calc(12rem * var(--tab-width-scale, 1));
|
||||
padding: 0.125rem 0;
|
||||
min-width: 0;
|
||||
border-radius: 0.25rem;
|
||||
|
|
@ -492,6 +537,160 @@
|
|||
0 0 24px rgb(242 63 66 / 50%);
|
||||
}
|
||||
|
||||
/* Pink Nitro glow effect (easter egg) */
|
||||
.vc-channeltabs-tab-nitro {
|
||||
animation: vc-channeltabs-nitro-pulse 3s ease-in-out infinite;
|
||||
box-shadow: 0 0 10px rgb(255 115 250 / 70%), 0 0 20px rgb(255 115 250 / 40%);
|
||||
}
|
||||
|
||||
@keyframes vc-channeltabs-nitro-pulse {
|
||||
|
||||
/* stylelint-disable-next-line rule-empty-line-before */
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 10px rgb(255 115 250 / 70%), 0 0 20px rgb(255 115 250 / 40%);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 0 16px rgb(255 115 250 / 90%), 0 0 32px rgb(255 115 250 / 60%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced Nitro glow for selected tab */
|
||||
.vc-channeltabs-tab-selected.vc-channeltabs-tab-nitro {
|
||||
box-shadow:
|
||||
0 1px 3px rgb(0 0 0 / 20%),
|
||||
0 0 16px rgb(255 115 250 / 90%),
|
||||
0 0 32px rgb(255 115 250 / 60%);
|
||||
}
|
||||
|
||||
/* Nitro icon gradient animation */
|
||||
.vc-channeltabs-tab-nitro .vc-channeltabs-tab-inner svg {
|
||||
filter: drop-shadow(0 0 2px rgb(255 115 250 / 80%));
|
||||
}
|
||||
|
||||
.vc-channeltabs-tab-nitro .vc-channeltabs-tab-inner svg path {
|
||||
fill: rgb(255 115 250) !important;
|
||||
animation: vc-channeltabs-nitro-icon-gradient 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes vc-channeltabs-nitro-icon-gradient {
|
||||
0%,
|
||||
100% {
|
||||
fill: rgb(255 115 250);
|
||||
}
|
||||
|
||||
50% {
|
||||
fill: rgb(255 180 250);
|
||||
}
|
||||
}
|
||||
|
||||
/* Quests active gradient (blue+pink) when quests are running */
|
||||
.vc-channeltabs-tab-quests-active {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.vc-channeltabs-tab-quests-active::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 11px;
|
||||
background: linear-gradient(135deg,
|
||||
transparent,
|
||||
transparent 25%,
|
||||
rgb(0 200 255) 40%,
|
||||
rgb(88 101 242) 50%,
|
||||
rgb(220 104 255) 60%,
|
||||
transparent 75%,
|
||||
transparent);
|
||||
background-size: 300% 300%;
|
||||
animation: vc-channeltabs-quests-diagonal 5s linear infinite;
|
||||
opacity: 0.8;
|
||||
filter: blur(2px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vc-channeltabs-tab-quests-active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 11px;
|
||||
animation: vc-channeltabs-quests-glow 3s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes vc-channeltabs-quests-diagonal {
|
||||
0%,
|
||||
40% {
|
||||
background-position: 100% 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
60%,
|
||||
100% {
|
||||
background-position: 0 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes vc-channeltabs-quests-glow {
|
||||
0% {
|
||||
box-shadow: 0 0 8px 1px rgb(0 200 255 / 60%);
|
||||
}
|
||||
|
||||
33% {
|
||||
box-shadow: 0 0 8px 1px rgb(88 101 242 / 60%);
|
||||
}
|
||||
|
||||
66% {
|
||||
box-shadow: 0 0 8px 1px rgb(220 104 255 / 60%);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 8px 1px rgb(0 200 255 / 60%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced gradient for selected Quests tab with active quests */
|
||||
.vc-channeltabs-tab-selected.vc-channeltabs-tab-quests-active::before {
|
||||
opacity: 1;
|
||||
filter: blur(3px);
|
||||
}
|
||||
|
||||
.vc-channeltabs-tab-selected.vc-channeltabs-tab-quests-active::after {
|
||||
animation: vc-channeltabs-quests-glow-selected 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes vc-channeltabs-quests-glow-selected {
|
||||
0% {
|
||||
box-shadow: 0 0 12px 2px rgb(0 200 255 / 80%);
|
||||
}
|
||||
|
||||
33% {
|
||||
box-shadow: 0 0 12px 2px rgb(88 101 242 / 80%);
|
||||
}
|
||||
|
||||
66% {
|
||||
box-shadow: 0 0 12px 2px rgb(220 104 255 / 80%);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 12px 2px rgb(0 200 255 / 80%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Disable Quests active animations when setting is off */
|
||||
.vc-channeltabs-container.vc-channeltabs-no-quests-active-animation .vc-channeltabs-tab-quests-active::before,
|
||||
.vc-channeltabs-container.vc-channeltabs-no-quests-active-animation .vc-channeltabs-tab-quests-active::after {
|
||||
animation: none !important;
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
/* Tab closing animation */
|
||||
@keyframes vc-channeltabs-tab-close {
|
||||
0% {
|
||||
|
|
@ -647,6 +846,13 @@
|
|||
animation: none !important;
|
||||
}
|
||||
|
||||
/* Also disable closing animation for instant disappear */
|
||||
.vc-channeltabs-container.vc-channeltabs-no-enter-exit-animation .vc-channeltabs-tab-closing {
|
||||
animation: none !important;
|
||||
opacity: 0 !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.vc-channeltabs-container.vc-channeltabs-no-enter-exit-animation .vc-channeltabs-tab {
|
||||
transition: opacity 0.2s ease-out,
|
||||
transform 0.2s ease-in-out,
|
||||
|
|
@ -730,6 +936,31 @@
|
|||
transition: none !important;
|
||||
}
|
||||
|
||||
/* Also disable tab-inner padding transition */
|
||||
.vc-channeltabs-container.vc-channeltabs-no-compact-animation .vc-channeltabs-tab-compact .vc-channeltabs-tab-inner {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
/* Make close button appear instantly with more specificity */
|
||||
.vc-channeltabs-container.vc-channeltabs-no-compact-animation .vc-channeltabs-tab-compact:hover .vc-channeltabs-close-button-compact,
|
||||
.vc-channeltabs-container.vc-channeltabs-no-compact-animation .vc-channeltabs-tab-compact.vc-channeltabs-tab-selected .vc-channeltabs-close-button-compact {
|
||||
transition: none !important;
|
||||
transition-delay: 0s !important;
|
||||
}
|
||||
|
||||
/* Tab positioning - disable if setting is off for INSTANT position changes */
|
||||
.vc-channeltabs-container.vc-channeltabs-no-tab-positioning .vc-channeltabs-tab-container {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.vc-channeltabs-container.vc-channeltabs-no-tab-positioning .vc-channeltabs-tab {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.vc-channeltabs-container.vc-channeltabs-no-tab-positioning .vc-channeltabs-tab-container:has(.vc-channeltabs-tab-dragging) .vc-channeltabs-tab:not(.vc-channeltabs-tab-dragging) {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
/* Compact auto-expand selected - disable if setting is off */
|
||||
.vc-channeltabs-container.vc-channeltabs-no-compact-auto-expand .vc-channeltabs-tab-compact.vc-channeltabs-tab-selected .vc-channeltabs-tab-inner {
|
||||
padding-right: 0 !important;
|
||||
|
|
@ -746,3 +977,74 @@
|
|||
opacity: 0 !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Compact hover expansion - disable if setting is off */
|
||||
.vc-channeltabs-container.vc-channeltabs-no-compact-hover-expand .vc-channeltabs-tab-compact:hover .vc-channeltabs-tab-inner {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.vc-channeltabs-container.vc-channeltabs-no-compact-hover-expand .vc-channeltabs-tab-compact:hover .vc-channeltabs-name-text {
|
||||
max-width: 0 !important;
|
||||
opacity: 0 !important;
|
||||
transform: translateX(-20px) !important;
|
||||
}
|
||||
|
||||
/* Hide close button on hover for compact tabs when hover-expand is disabled */
|
||||
.vc-channeltabs-container.vc-channeltabs-no-compact-hover-expand .vc-channeltabs-tab-compact:hover .vc-channeltabs-close-button-compact {
|
||||
opacity: 0 !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Enhanced caching mode - smoother transitions */
|
||||
[class*="chat_"] {
|
||||
transition: opacity 0.1s ease;
|
||||
}
|
||||
|
||||
/* Prevent flash when switching tabs with cache enabled */
|
||||
[class*="scrollerInner_"] {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
/* Drag and drop visual feedback */
|
||||
.vc-channeltabs-bookmarks-drop-target {
|
||||
background-color: color-mix(in srgb, var(--brand-500) 20%, transparent);
|
||||
border: 2px dashed var(--brand-500);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.vc-channeltabs-bookmark.vc-channeltabs-folder-drop-target {
|
||||
background-color: color-mix(in srgb, var(--brand-500) 30%, transparent);
|
||||
border: 2px solid var(--brand-500);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 8px var(--brand-500);
|
||||
}
|
||||
|
||||
/* Folder expansion */
|
||||
.vc-channeltabs-bookmark.vc-channeltabs-folder-expanded {
|
||||
background-color: var(--background-modifier-selected);
|
||||
}
|
||||
|
||||
/* Folder item styling - indented and visually connected to folder */
|
||||
.vc-channeltabs-bookmark.vc-channeltabs-folder-item {
|
||||
margin-left: 0.5rem;
|
||||
border-left: 2px solid var(--background-modifier-accent);
|
||||
padding-left: 0.5rem;
|
||||
opacity: 0.9;
|
||||
background-color: var(--background-secondary-alt);
|
||||
}
|
||||
|
||||
.vc-channeltabs-bookmark.vc-channeltabs-folder-item:hover {
|
||||
opacity: 1;
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
/* Active bookmark visual indicator */
|
||||
.vc-channeltabs-bookmark.vc-channeltabs-bookmark-active {
|
||||
border: 1px solid var(--brand-500);
|
||||
box-shadow: 0 0 4px var(--brand-500);
|
||||
background-color: color-mix(in srgb, var(--brand-500) 15%, transparent);
|
||||
}
|
||||
|
||||
.vc-channeltabs-bookmark.vc-channeltabs-bookmark-active:hover {
|
||||
box-shadow: 0 0 6px var(--brand-500);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,25 @@ export function isBookmarkFolder(bookmark: Bookmark | BookmarkFolder): bookmark
|
|||
|
||||
export function bookmarkPlaceholderName(bookmark: Omit<Bookmark | BookmarkFolder, "name">) {
|
||||
if (isBookmarkFolder(bookmark as Bookmark | BookmarkFolder)) return "Folder";
|
||||
const channel = ChannelStore.getChannel((bookmark as Bookmark).channelId);
|
||||
|
||||
const { channelId } = (bookmark as Bookmark);
|
||||
|
||||
// handle special synthetic pages
|
||||
if (channelId?.startsWith("__")) {
|
||||
const specialPagesMap: Record<string, string> = {
|
||||
"__quests__": "Quests",
|
||||
"__message-requests__": "Message Requests",
|
||||
"__friends__": "Friends",
|
||||
"__shop__": "Shop",
|
||||
"__library__": "Library",
|
||||
"__discovery__": "Discovery",
|
||||
"__nitro__": "Nitro"
|
||||
};
|
||||
|
||||
return specialPagesMap[channelId] || "Special Page";
|
||||
}
|
||||
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
|
||||
if (!channel) return "Bookmark";
|
||||
if (channel.name) return `#${channel.name}`;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { OptionType } from "@utils/types";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { Forms, SearchableSelect, useState } from "@webpack/common";
|
||||
import { JSX } from "react";
|
||||
|
||||
|
|
@ -32,7 +31,10 @@ function AnimationSettings(): JSX.Element {
|
|||
{ label: "Compact Mode Expansion", value: "compact-expand", selected: settings.store.animationCompactExpand },
|
||||
{ label: "Selected Tab Blue Border", value: "selected-border", selected: settings.store.animationSelectedBorder },
|
||||
{ label: "Selected Tab Background Color", value: "selected-background", selected: settings.store.animationSelectedBackground },
|
||||
{ label: "Tab Shadow Effects", value: "tab-shadows", selected: settings.store.animationTabShadows }
|
||||
{ label: "Tab Shadow Effects", value: "tab-shadows", selected: settings.store.animationTabShadows },
|
||||
{ label: "Tab Repositioning (smooth position changes)", value: "tab-positioning", selected: settings.store.animationTabPositioning },
|
||||
{ label: "Resize Handle Fade", value: "resize-handle", selected: settings.store.animationResizeHandle },
|
||||
{ label: "Active Quests Gradient", value: "quests-active", selected: settings.store.animationQuestsActive }
|
||||
];
|
||||
|
||||
const [currentValue, setCurrentValue] = useState(animationOptions.filter(option => option.selected));
|
||||
|
|
@ -56,6 +58,9 @@ function AnimationSettings(): JSX.Element {
|
|||
settings.store.animationSelectedBorder = enabledValues.includes("selected-border");
|
||||
settings.store.animationSelectedBackground = enabledValues.includes("selected-background");
|
||||
settings.store.animationTabShadows = enabledValues.includes("tab-shadows");
|
||||
settings.store.animationTabPositioning = enabledValues.includes("tab-positioning");
|
||||
settings.store.animationResizeHandle = enabledValues.includes("resize-handle");
|
||||
settings.store.animationQuestsActive = enabledValues.includes("quests-active");
|
||||
|
||||
setCurrentValue(enabled);
|
||||
}
|
||||
|
|
@ -160,6 +165,19 @@ export const settings = definePluginSettings({
|
|||
default: false,
|
||||
restartNeeded: false
|
||||
},
|
||||
tabWidthScale: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "Tab width scale (percentage) - adjustable by dragging tab edges",
|
||||
default: 100,
|
||||
hidden: true,
|
||||
restartNeeded: false
|
||||
},
|
||||
renderAllTabs: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Keep all tabs cached in memory for faster switching (caches scroll position and state)",
|
||||
default: false,
|
||||
restartNeeded: false
|
||||
},
|
||||
switchToExistingTab: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Switch to tab if it already exists for the channel you're navigating to",
|
||||
|
|
@ -284,18 +302,52 @@ export const settings = definePluginSettings({
|
|||
default: true,
|
||||
hidden: true
|
||||
},
|
||||
animationTabPositioning: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable smooth transitions when tabs move positions",
|
||||
default: true,
|
||||
hidden: true
|
||||
},
|
||||
animationResizeHandle: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable fade animation for resize handle",
|
||||
default: true,
|
||||
hidden: true
|
||||
},
|
||||
animationQuestsActive: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable gradient animations on Quests tab when quests are actively running",
|
||||
default: true,
|
||||
hidden: true
|
||||
},
|
||||
compactAutoExpandSelected: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Automatically expand compact tabs when selected to show the full channel name",
|
||||
default: true,
|
||||
restartNeeded: false
|
||||
},
|
||||
compactAutoExpandOnHover: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Automatically expand compact tabs on hover to show the full channel name",
|
||||
default: true,
|
||||
restartNeeded: false
|
||||
},
|
||||
openInNewTabAutoSwitch: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Automatically switch to new tabs opened from 'Open in New Tab' context menu",
|
||||
default: true,
|
||||
restartNeeded: false
|
||||
},
|
||||
bookmarksIndependentFromTabs: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Bookmarks navigate independently without affecting the active tabs bar",
|
||||
default: true,
|
||||
restartNeeded: false
|
||||
},
|
||||
showResizeHandle: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show resize handle when hovering over tabs to adjust tab width",
|
||||
default: true,
|
||||
restartNeeded: false
|
||||
}
|
||||
});
|
||||
|
||||
export const CircleQuestionIcon = findComponentByCodeLazy("10.58l-3.3-3.3a1");
|
||||
|
|
|
|||
16
src/equicordplugins/channelTabs/util/icons.ts
Normal file
16
src/equicordplugins/channelTabs/util/icons.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
|
||||
// icons n shi
|
||||
export const QuestIcon = findComponentByCodeLazy("10.47a.76.76");
|
||||
export const ShopIcon = findComponentByCodeLazy("M2.63 4.19A3");
|
||||
export const EnvelopeIcon = findComponentByCodeLazy("M1.16 5.02c-.1.28");
|
||||
export const DiscoveryIcon = findComponentByCodeLazy("M7.74 9.3A2 2 0 0 1 9.3 7.75l7.22");
|
||||
export const NitroIcon = findComponentByCodeLazy("M16.23 12c0 1.29-.95 2.25");
|
||||
export const FriendsIcon = findComponentByCodeLazy("12h1a8");
|
||||
export const CircleQuestionIcon = findComponentByCodeLazy("10.58l-3.3-3.3a1");
|
||||
|
|
@ -6,5 +6,6 @@
|
|||
|
||||
export * from "./bookmarks";
|
||||
export * from "./constants";
|
||||
export * from "./icons";
|
||||
export * from "./tabs";
|
||||
export * from "./types";
|
||||
|
|
|
|||
|
|
@ -14,6 +14,65 @@ import { BasicChannelTabsProps, ChannelTabsProps, PersistedTabs } from "./types"
|
|||
|
||||
const cl = classNameFactory("vc-channeltabs-");
|
||||
|
||||
// Navigation context tracking system
|
||||
interface NavigationContext {
|
||||
guildId: string;
|
||||
channelId: string;
|
||||
timestamp: number;
|
||||
source: "bookmark" | "tab" | "unknown";
|
||||
}
|
||||
|
||||
let lastNavigationContext: NavigationContext | null = null;
|
||||
let isViewingViaBookmark = false;
|
||||
let isNavigatingViaTabFlag = false;
|
||||
|
||||
export function isViewingViaBookmarkMode() {
|
||||
return settings.store.bookmarksIndependentFromTabs && isViewingViaBookmark;
|
||||
}
|
||||
|
||||
export function isNavigatingViaTab() {
|
||||
return isNavigatingViaTabFlag;
|
||||
}
|
||||
|
||||
export function clearBookmarkViewingMode() {
|
||||
isViewingViaBookmark = false;
|
||||
}
|
||||
|
||||
export function setNavigationSource(guildId: string, channelId: string, source: "bookmark" | "tab") {
|
||||
lastNavigationContext = {
|
||||
guildId: guildId || "@me",
|
||||
channelId,
|
||||
timestamp: Date.now(),
|
||||
source
|
||||
};
|
||||
}
|
||||
|
||||
export function isNavigationFromSource(guildId: string | null | undefined, channelId: string, source: "bookmark"): boolean {
|
||||
if (!lastNavigationContext) return false;
|
||||
|
||||
// Normalize both sides for comparison (handles null/undefined/"" for DMs)
|
||||
const normalizedGuildId = guildId || "@me";
|
||||
const normalizedContextGuildId = lastNavigationContext.guildId || "@me";
|
||||
|
||||
// Check if this navigation matches the tracked source
|
||||
const matches =
|
||||
normalizedContextGuildId === normalizedGuildId &&
|
||||
lastNavigationContext.channelId === channelId &&
|
||||
lastNavigationContext.source === source;
|
||||
|
||||
// Don't clear immediately - allow multiple React updates to check
|
||||
// Context will be cleared by clearStaleNavigationContext() after 2 seconds
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
// Clean up old contexts (safety mechanism)
|
||||
export function clearStaleNavigationContext() {
|
||||
if (lastNavigationContext && Date.now() - lastNavigationContext.timestamp > 2000) {
|
||||
lastNavigationContext = null;
|
||||
}
|
||||
}
|
||||
|
||||
function replaceArray<T>(array: T[], ...values: T[]) {
|
||||
const len = array.length;
|
||||
for (let i = 0; i < len; i++) array.pop();
|
||||
|
|
@ -29,6 +88,13 @@ let currentlyOpenTab: number;
|
|||
const openTabHistory: number[] = [];
|
||||
let persistedTabs: Promise<PersistedTabs | undefined>;
|
||||
|
||||
// cache for the tab state (so like scroll pos etc)
|
||||
interface TabStateCache {
|
||||
scrollPosition: number;
|
||||
timestamp: number;
|
||||
}
|
||||
const tabStateCache = new Map<number, TabStateCache>();
|
||||
|
||||
// horror
|
||||
const _ = {
|
||||
get openedTabs() {
|
||||
|
|
@ -63,6 +129,9 @@ export function closeTab(id: number) {
|
|||
const closed = openTabs.splice(i, 1);
|
||||
closedTabs.push(...closed);
|
||||
|
||||
// the memory leak preventer
|
||||
tabStateCache.delete(id);
|
||||
|
||||
if (id === currentlyOpenTab) {
|
||||
if (openTabHistory.length) {
|
||||
openTabHistory.pop();
|
||||
|
|
@ -131,6 +200,14 @@ export function closeTabsToTheLeft(id: number) {
|
|||
let lastNavigationTime = 0;
|
||||
|
||||
export function handleChannelSwitch(ch: BasicChannelTabsProps) {
|
||||
// only require channelId to allow synthetic IDs for special pages
|
||||
if (!ch.channelId) return;
|
||||
|
||||
// don't modify tabs when viewing via bookmarks in independent mode
|
||||
if (isViewingViaBookmarkMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tab = openTabs.find(c => c.id === currentlyOpenTab);
|
||||
|
||||
// Normalize guildId for DMs/Group Chats to ensure consistent comparison
|
||||
|
|
@ -183,6 +260,10 @@ export function hasClosedTabs() {
|
|||
}
|
||||
|
||||
export function isTabSelected(id: number) {
|
||||
// don't show any tab as selected when viewing via bookmark in independent mode
|
||||
if (isViewingViaBookmark && settings.store.bookmarksIndependentFromTabs) {
|
||||
return false;
|
||||
}
|
||||
return id === currentlyOpenTab;
|
||||
}
|
||||
|
||||
|
|
@ -197,18 +278,93 @@ export function moveDraggedTabs(index1: number, index2: number) {
|
|||
update();
|
||||
}
|
||||
|
||||
function getScrollContainer(): HTMLElement | null {
|
||||
// discord's main chat scroller
|
||||
return document.querySelector('[class*="scrollerInner_"]') as HTMLElement;
|
||||
}
|
||||
|
||||
function cacheCurrentTabState() {
|
||||
if (!settings.store.renderAllTabs) return;
|
||||
|
||||
const scrollContainer = getScrollContainer();
|
||||
if (scrollContainer && currentlyOpenTab !== undefined) {
|
||||
tabStateCache.set(currentlyOpenTab, {
|
||||
scrollPosition: scrollContainer.scrollTop,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function restoreTabState(tabId: number) {
|
||||
if (!settings.store.renderAllTabs) return;
|
||||
|
||||
const cached = tabStateCache.get(tabId);
|
||||
if (!cached) return;
|
||||
|
||||
// restore scroll pos after delay to make sure content loaded
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
const scrollContainer = getScrollContainer();
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTop = cached.scrollPosition;
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
export function moveToTab(id: number) {
|
||||
const tab = openTabs.find(v => v.id === id);
|
||||
if (tab === undefined) return logger.error("Couldn't find channel tab with ID " + id, openTabs);
|
||||
|
||||
// clear bookmark viewing mode when switching to a tab
|
||||
isViewingViaBookmark = false;
|
||||
|
||||
// cache current tab state before switching to it
|
||||
cacheCurrentTabState();
|
||||
|
||||
setOpenTab(id);
|
||||
|
||||
// SET FLAG: We're initiating navigation
|
||||
isNavigatingViaTabFlag = true;
|
||||
|
||||
// handle special pages with synthetic channelIds
|
||||
if (tab.channelId && tab.channelId.startsWith("__")) {
|
||||
const routeMap: Record<string, string> = {
|
||||
"__quests__": "/quest-home",
|
||||
"__message-requests__": "/message-requests",
|
||||
"__friends__": "/channels/@me",
|
||||
"__shop__": "/shop",
|
||||
"__library__": "/library",
|
||||
"__discovery__": "/discovery",
|
||||
"__nitro__": "/store"
|
||||
};
|
||||
|
||||
const route = routeMap[tab.channelId];
|
||||
if (route) {
|
||||
setNavigationSource(tab.guildId, tab.channelId, "tab");
|
||||
NavigationRouter.transitionTo(route);
|
||||
// Clear flag after navigation completes
|
||||
setTimeout(() => { isNavigatingViaTabFlag = false; }, 100);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// regular channel nav
|
||||
if (tab.messageId) {
|
||||
setNavigationSource(tab.guildId, tab.channelId, "tab");
|
||||
NavigationRouter.transitionTo(`/channels/${tab.guildId}/${tab.channelId}/${tab.messageId}`);
|
||||
delete openTabs[openTabs.indexOf(tab)].messageId;
|
||||
}
|
||||
else if (tab.channelId !== SelectedChannelStore.getChannelId() || tab.guildId !== SelectedGuildStore.getGuildId())
|
||||
else if (tab.channelId !== SelectedChannelStore.getChannelId() || tab.guildId !== SelectedGuildStore.getGuildId()) {
|
||||
setNavigationSource(tab.guildId, tab.channelId, "tab");
|
||||
NavigationRouter.transitionToGuild(tab.guildId, tab.channelId);
|
||||
// restore cached state for the new tab
|
||||
restoreTabState(id);
|
||||
}
|
||||
else update();
|
||||
|
||||
// Clear flag after navigation
|
||||
setTimeout(() => { isNavigatingViaTabFlag = false; }, 100);
|
||||
}
|
||||
|
||||
export function openStartupTabs(props: BasicChannelTabsProps & { userId: string; }, setUserId: (id: string) => void) {
|
||||
|
|
@ -290,6 +446,48 @@ export function switchChannel(ch: BasicChannelTabsProps) {
|
|||
moveToTab(openTabs.find(t => t.id === currentlyOpenTab)!.id);
|
||||
}
|
||||
|
||||
export function navigateToBookmark(ch: BasicChannelTabsProps) {
|
||||
// check if independent mode is enabled
|
||||
if (settings.store.bookmarksIndependentFromTabs) {
|
||||
// set flag that we're viewing via bookmark
|
||||
isViewingViaBookmark = true;
|
||||
|
||||
// set navigation context BEFORE navigating
|
||||
setNavigationSource(ch.guildId, ch.channelId, "bookmark");
|
||||
|
||||
// Handle special pages with synthetic channelIds
|
||||
if (ch.channelId && ch.channelId.startsWith("__")) {
|
||||
const routeMap: Record<string, string> = {
|
||||
"__quests__": "/quest-home",
|
||||
"__message-requests__": "/message-requests",
|
||||
"__friends__": "/channels/@me",
|
||||
"__shop__": "/shop",
|
||||
"__library__": "/library",
|
||||
"__discovery__": "/discovery",
|
||||
"__nitro__": "/store"
|
||||
};
|
||||
|
||||
const route = routeMap[ch.channelId];
|
||||
if (route) {
|
||||
NavigationRouter.transitionTo(route);
|
||||
// trigger update to reflect tab deselection
|
||||
update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// navigate directly without affecting tab state
|
||||
NavigationRouter.transitionToGuild(ch.guildId, ch.channelId);
|
||||
|
||||
// trigger update to reflect tab deselection
|
||||
update();
|
||||
} else {
|
||||
// use old behavior - affects tabs
|
||||
// sorry for all the comments im losing it here
|
||||
switchChannel(ch);
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleCompactTab(id: number) {
|
||||
const i = openTabs.findIndex(v => v.id === id);
|
||||
if (i === -1) return logger.error("Couldn't find channel tab with ID " + id, openTabs);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue