feat/channelTabs (#483)
* holy addition gang * performance optimization * padding fix * Update KeybindSettings.tsx * Update tabs.tsx --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
parent
f48b7f3702
commit
edb643fac2
7 changed files with 810 additions and 64 deletions
|
|
@ -99,6 +99,32 @@ export const NotificationDot = ({ channelIds }: { channelIds: string[]; }) => {
|
|||
</div> : null;
|
||||
};
|
||||
|
||||
interface TabNumberBadgeProps {
|
||||
number: number;
|
||||
position: "left" | "right";
|
||||
isSelected: boolean;
|
||||
isCompact: boolean;
|
||||
isHovered: boolean;
|
||||
}
|
||||
|
||||
export const TabNumberBadge = ({ number, position, isSelected, isCompact, isHovered }: TabNumberBadgeProps) => {
|
||||
// hide badge if:
|
||||
// 1. tab is currently selected
|
||||
// 2. tab is compact AND not hovered
|
||||
const shouldHide = isSelected || (isCompact && !isHovered);
|
||||
|
||||
if (shouldHide) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cl("tab-number-badge", `position-${position}`)}
|
||||
data-position={position}
|
||||
>
|
||||
{number}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function ChannelTabContent(props: ChannelTabsProps & {
|
||||
guild?: Guild,
|
||||
channel?: Channel;
|
||||
|
|
@ -247,6 +273,9 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
|
|||
const [isClosing, setIsClosing] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isDropTarget, setIsDropTarget] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const { showTabNumbers, tabNumberPosition } = settings.use(["showTabNumbers", "tabNumberPosition"]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEntering) {
|
||||
|
|
@ -273,6 +302,7 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
|
|||
const startX = e.clientX;
|
||||
const startWidth = ref.current?.getBoundingClientRect().width || 0;
|
||||
const baseWidth = 192; // 12rem in pixels (assuming 16px base font)
|
||||
let pendingScale = settings.store.tabWidthScale;
|
||||
|
||||
document.body.style.cursor = "ew-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
|
|
@ -284,7 +314,12 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
|
|||
|
||||
// 50% and 200% scale
|
||||
const clampedScale = Math.max(0.5, Math.min(2, newScale));
|
||||
settings.store.tabWidthScale = Math.round(clampedScale * 100);
|
||||
pendingScale = Math.round(clampedScale * 100);
|
||||
|
||||
// update CSS variable immediately for visual feedback
|
||||
if (ref.current) {
|
||||
ref.current.style.setProperty("--tab-width-scale", String(pendingScale / 100));
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
|
|
@ -292,6 +327,12 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
|
|||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
|
||||
settings.store.tabWidthScale = pendingScale;
|
||||
|
||||
if (ref.current) {
|
||||
ref.current.style.removeProperty("--tab-width-scale");
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
|
|
@ -398,6 +439,8 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
|
|||
})}
|
||||
key={index}
|
||||
ref={ref}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onAuxClick={e => {
|
||||
if (e.button === 1 /* middle click */)
|
||||
closeTab(id);
|
||||
|
|
@ -412,7 +455,29 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
|
|||
className={cl("tab-inner")}
|
||||
data-compact={compact}
|
||||
>
|
||||
{/* left position badge */}
|
||||
{showTabNumbers && tabNumberPosition === "left" && (
|
||||
<TabNumberBadge
|
||||
number={index + 1}
|
||||
position="left"
|
||||
isSelected={isTabSelected(id)}
|
||||
isCompact={compact}
|
||||
isHovered={isHovered}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ChannelTabContent {...props} guild={guild} channel={channel} />
|
||||
|
||||
{/* right position badge */}
|
||||
{showTabNumbers && tabNumberPosition === "right" && (
|
||||
<TabNumberBadge
|
||||
number={index + 1}
|
||||
position="right"
|
||||
isSelected={isTabSelected(id)}
|
||||
isCompact={compact}
|
||||
isHovered={isHovered}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { useForceUpdater } from "@utils/react";
|
|||
import { findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Button, ContextMenuApi, Flex, FluxDispatcher, useCallback, useEffect, useRef, UserStore, useState, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import { BasicChannelTabsProps, ChannelTabsProps, clearStaleNavigationContext, createTab, handleChannelSwitch, isNavigationFromSource, moveToTab, openedTabs, openStartupTabs, saveTabs, settings, setUpdaterFunction, useGhostTabs } from "../util";
|
||||
import { BasicChannelTabsProps, ChannelTabsProps, clearStaleNavigationContext, closeTab, createTab, handleChannelSwitch, isNavigationFromSource, isTabSelected, moveToTab, openedTabs, openStartupTabs, saveTabs, settings, setUpdaterFunction, useGhostTabs } from "../util";
|
||||
import BookmarkContainer from "./BookmarkContainer";
|
||||
import ChannelTab, { PreviewTab } from "./ChannelTab";
|
||||
import { BasicContextMenu } from "./ContextMenus";
|
||||
|
|
@ -31,8 +31,15 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
|
|||
showBookmarkBar,
|
||||
widerTabsAndBookmarks,
|
||||
tabWidthScale,
|
||||
enableHotkeys,
|
||||
hotkeyCount,
|
||||
enableNumberKeySwitching,
|
||||
numberKeySwitchCount,
|
||||
enableCloseTabShortcut,
|
||||
enableNewTabShortcut,
|
||||
enableTabCycleShortcut,
|
||||
closeTabKeybind,
|
||||
newTabKeybind,
|
||||
cycleTabForwardKeybind,
|
||||
cycleTabBackwardKeybind,
|
||||
tabBarPosition,
|
||||
animationHover,
|
||||
animationSelection,
|
||||
|
|
@ -55,8 +62,15 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
|
|||
"showBookmarkBar",
|
||||
"widerTabsAndBookmarks",
|
||||
"tabWidthScale",
|
||||
"enableHotkeys",
|
||||
"hotkeyCount",
|
||||
"enableNumberKeySwitching",
|
||||
"numberKeySwitchCount",
|
||||
"enableCloseTabShortcut",
|
||||
"enableNewTabShortcut",
|
||||
"enableTabCycleShortcut",
|
||||
"closeTabKeybind",
|
||||
"newTabKeybind",
|
||||
"cycleTabForwardKeybind",
|
||||
"cycleTabBackwardKeybind",
|
||||
"tabBarPosition",
|
||||
"animationHover",
|
||||
"animationSelection",
|
||||
|
|
@ -86,6 +100,7 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
|
|||
}, [userId]);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const tabScrollerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setUpdaterFunction(update);
|
||||
|
|
@ -115,11 +130,35 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
|
|||
_update();
|
||||
}, [widerTabsAndBookmarks]);
|
||||
useEffect(() => {
|
||||
if (!enableHotkeys) return;
|
||||
const matchesKeybind = (event: KeyboardEvent, keybindString: string): boolean => {
|
||||
const parts = keybindString.split("+");
|
||||
const hasCtrl = parts.includes("CTRL");
|
||||
const hasShift = parts.includes("SHIFT");
|
||||
const hasAlt = parts.includes("ALT");
|
||||
const mainKey = parts[parts.length - 1].toLowerCase();
|
||||
|
||||
const ctrlPressed = event.ctrlKey || event.metaKey;
|
||||
const shiftPressed = event.shiftKey;
|
||||
const altPressed = event.altKey;
|
||||
const keyPressed = event.key.toLowerCase();
|
||||
|
||||
// special handling for TAB key
|
||||
if (mainKey === "tab") {
|
||||
return hasCtrl === ctrlPressed && hasShift === shiftPressed && hasAlt === altPressed && keyPressed === "tab";
|
||||
}
|
||||
|
||||
// special handling for SPACE
|
||||
if (mainKey === "space") {
|
||||
return hasCtrl === ctrlPressed && hasShift === shiftPressed && hasAlt === altPressed && keyPressed === " ";
|
||||
}
|
||||
|
||||
return hasCtrl === ctrlPressed && hasShift === shiftPressed && hasAlt === altPressed && keyPressed === mainKey;
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// skip if typing in input fields
|
||||
if (
|
||||
target.tagName === "INPUT" ||
|
||||
target.tagName === "TEXTAREA" ||
|
||||
|
|
@ -128,23 +167,80 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
|
|||
return;
|
||||
}
|
||||
|
||||
const keyNumber = parseInt(event.key, 10);
|
||||
|
||||
if (!isNaN(keyNumber) && keyNumber >= 1 && keyNumber <= hotkeyCount) {
|
||||
const tabIndex = keyNumber - 1;
|
||||
if (openedTabs[tabIndex]) {
|
||||
event.preventDefault();
|
||||
moveToTab(openedTabs[tabIndex].id);
|
||||
// 1. number key switching (1-9)
|
||||
if (enableNumberKeySwitching) {
|
||||
const keyNumber = parseInt(event.key, 10);
|
||||
if (!isNaN(keyNumber) && keyNumber >= 1 && keyNumber <= numberKeySwitchCount) {
|
||||
const tabIndex = keyNumber - 1;
|
||||
if (openedTabs[tabIndex]) {
|
||||
event.preventDefault();
|
||||
moveToTab(openedTabs[tabIndex].id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. close tab shortcut (default: CTRL+W)
|
||||
if (enableCloseTabShortcut && matchesKeybind(event, closeTabKeybind)) {
|
||||
event.preventDefault();
|
||||
const currentTab = openedTabs.find(t => isTabSelected(t.id));
|
||||
if (currentTab && openedTabs.length > 1) {
|
||||
closeTab(currentTab.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. new tab shortcut (default: CTRL+T)
|
||||
if (enableNewTabShortcut && matchesKeybind(event, newTabKeybind)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation(); // prevent discord's quick switcher from seeing this
|
||||
createTab(props, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. cycle tabs forward (default: CTRL+TAB)
|
||||
if (enableTabCycleShortcut && matchesKeybind(event, cycleTabForwardKeybind)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation(); // prevent discord's guild switcher from seeing this
|
||||
const currentIndex = openedTabs.findIndex(t => isTabSelected(t.id));
|
||||
if (currentIndex !== -1 && openedTabs.length > 1) {
|
||||
const nextIndex = (currentIndex + 1) % openedTabs.length;
|
||||
moveToTab(openedTabs[nextIndex].id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. cycle tabs backward (default: CTRL+SHIFT+TAB)
|
||||
if (enableTabCycleShortcut && matchesKeybind(event, cycleTabBackwardKeybind)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation(); // prevent Discord's guild switcher from seeing this
|
||||
const currentIndex = openedTabs.findIndex(t => isTabSelected(t.id));
|
||||
if (currentIndex !== -1 && openedTabs.length > 1) {
|
||||
const nextIndex = (currentIndex - 1 + openedTabs.length) % openedTabs.length;
|
||||
moveToTab(openedTabs[nextIndex].id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
document.addEventListener("keydown", handleKeyDown, true);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
document.removeEventListener("keydown", handleKeyDown, true);
|
||||
};
|
||||
}, [enableHotkeys, hotkeyCount]);
|
||||
}, [
|
||||
enableNumberKeySwitching,
|
||||
numberKeySwitchCount,
|
||||
enableCloseTabShortcut,
|
||||
closeTabKeybind,
|
||||
enableNewTabShortcut,
|
||||
newTabKeybind,
|
||||
enableTabCycleShortcut,
|
||||
cycleTabForwardKeybind,
|
||||
cycleTabBackwardKeybind,
|
||||
props,
|
||||
openedTabs
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
|
|
@ -200,9 +296,11 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
|
|||
<div className={cl("separator")} />
|
||||
</>}
|
||||
<div className={cl("tab-container")}>
|
||||
{openedTabs.filter(tab => tab != null).map((tab, i) =>
|
||||
<ChannelTab {...tab} index={i} key={tab.id} />
|
||||
)}
|
||||
<div className={cl("tab-scroller")} ref={tabScrollerRef}>
|
||||
{openedTabs.filter(tab => tab != null).map((tab, i) =>
|
||||
<ChannelTab {...tab} index={i} key={tab.id} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => createTab(props, true)}
|
||||
|
|
|
|||
229
src/equicordplugins/channelTabs/components/KeybindSettings.tsx
Normal file
229
src/equicordplugins/channelTabs/components/KeybindSettings.tsx
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Divider } from "@components/Divider";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Heading } from "@components/Heading";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { IS_MAC } from "@utils/constants";
|
||||
import { Button, React, Text, useEffect, useRef, useState } from "@webpack/common";
|
||||
import { JSX } from "react";
|
||||
|
||||
import { settings } from "../util";
|
||||
|
||||
interface KeybindInputProps {
|
||||
label: string;
|
||||
description: string;
|
||||
settingKey: "closeTabKeybind" | "newTabKeybind" | "cycleTabForwardKeybind" | "cycleTabBackwardKeybind";
|
||||
enabledKey?: "enableCloseTabShortcut" | "enableNewTabShortcut" | "enableTabCycleShortcut";
|
||||
}
|
||||
|
||||
function KeybindInput({ label, description, settingKey, enabledKey }: KeybindInputProps) {
|
||||
const settingsValues = settings.use([settingKey]);
|
||||
const currentKeybind = settingsValues[settingKey];
|
||||
const [isListening, setIsListening] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isListening) return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Ignore if only modifier keys are pressed
|
||||
if (["Control", "Shift", "Alt", "Meta"].includes(event.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build keybind string
|
||||
const keys: string[] = [];
|
||||
if (IS_MAC && event.ctrlKey) {
|
||||
if (event.ctrlKey) keys.push("CONTROL");
|
||||
if (event.metaKey) keys.push("CTRL");
|
||||
} else if (event.ctrlKey) {
|
||||
keys.push("CTRL");
|
||||
}
|
||||
if (event.shiftKey) keys.push("SHIFT");
|
||||
if (event.altKey) keys.push("ALT");
|
||||
|
||||
// Normalize the key name
|
||||
let mainKey = event.key.toUpperCase();
|
||||
if (mainKey === " ") mainKey = "SPACE";
|
||||
if (mainKey === "ESCAPE") mainKey = "ESC";
|
||||
|
||||
keys.push(mainKey);
|
||||
|
||||
const keybindString = keys.join("+");
|
||||
|
||||
// Check for conflicts with other keybinds
|
||||
const allKeybinds = {
|
||||
closeTabKeybind: settings.store.closeTabKeybind,
|
||||
newTabKeybind: settings.store.newTabKeybind,
|
||||
cycleTabForwardKeybind: settings.store.cycleTabForwardKeybind,
|
||||
cycleTabBackwardKeybind: settings.store.cycleTabBackwardKeybind
|
||||
};
|
||||
|
||||
// Check if this keybind is already used by another setting
|
||||
const conflictKey = Object.entries(allKeybinds).find(
|
||||
([key, value]) => key !== settingKey && value === keybindString
|
||||
);
|
||||
|
||||
if (conflictKey) {
|
||||
setError(`This keybind is already used by: ${conflictKey[0]}`);
|
||||
setTimeout(() => setError(null), 3000);
|
||||
setIsListening(false);
|
||||
return;
|
||||
}
|
||||
|
||||
settings.store[settingKey] = keybindString;
|
||||
setError(null);
|
||||
setIsListening(false);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsListening(false);
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown, true);
|
||||
window.addEventListener("blur", handleBlur);
|
||||
|
||||
buttonRef.current?.focus();
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown, true);
|
||||
window.removeEventListener("blur", handleBlur);
|
||||
};
|
||||
}, [isListening, settingKey]);
|
||||
|
||||
const handleReset = () => {
|
||||
const defaults = {
|
||||
closeTabKeybind: "CTRL+W",
|
||||
newTabKeybind: "CTRL+T",
|
||||
cycleTabForwardKeybind: "CTRL+TAB",
|
||||
cycleTabBackwardKeybind: "CTRL+SHIFT+TAB"
|
||||
};
|
||||
settings.store[settingKey] = defaults[settingKey];
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const isEnabled = enabledKey ? settings.use([enabledKey])[enabledKey] : true;
|
||||
|
||||
return (
|
||||
<div className="channelTabs-keybind-input">
|
||||
<div className="channelTabs-keybind-info">
|
||||
<Text variant="text-md/semibold">{label}</Text>
|
||||
<Text variant="text-sm/normal" style={{ color: "var(--text-muted)" }}>
|
||||
{description}
|
||||
</Text>
|
||||
{!isEnabled && (
|
||||
<Text variant="text-xs/normal" style={{ color: "var(--text-danger)" }}>
|
||||
This shortcut is currently disabled
|
||||
</Text>
|
||||
)}
|
||||
{error && (
|
||||
<Text variant="text-xs/normal" className="channelTabs-keybind-conflict">
|
||||
{error}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<div className="channelTabs-keybind-controls">
|
||||
<button
|
||||
ref={buttonRef}
|
||||
className={`channelTabs-keybind-button ${isListening ? "listening" : ""}`}
|
||||
onClick={() => setIsListening(true)}
|
||||
disabled={!isEnabled}
|
||||
>
|
||||
{isListening ? "Press any key..." : formatKeybind(currentKeybind)}
|
||||
</button>
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
color={Button.Colors.PRIMARY}
|
||||
onClick={handleReset}
|
||||
disabled={!isEnabled}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatKeybind(keybind: string): string {
|
||||
const isMac = navigator.platform.toUpperCase().includes("MAC");
|
||||
|
||||
if (!isMac) {
|
||||
return keybind;
|
||||
}
|
||||
|
||||
// special case: CTRL+TAB must stay as CTRL on Mac (⌘+TAB is system app switcher)
|
||||
if (keybind.includes("CTRL+TAB") || keybind.includes("CTRL+SHIFT+TAB")) {
|
||||
return keybind.replace("CTRL", "^"); // Show control symbol on Mac
|
||||
}
|
||||
|
||||
// for other shortcuts, replace CTRL with CMD symbol on Mac
|
||||
return keybind.replace("CTRL", "⌘");
|
||||
// this is such a bad way to do this but i dont know man
|
||||
}
|
||||
|
||||
export function KeybindSettings(): JSX.Element {
|
||||
const handleResetAll = () => {
|
||||
settings.store.closeTabKeybind = "CTRL+W";
|
||||
settings.store.newTabKeybind = "CTRL+T";
|
||||
settings.store.cycleTabForwardKeybind = "CTRL+TAB";
|
||||
settings.store.cycleTabBackwardKeybind = "CTRL+SHIFT+TAB";
|
||||
};
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Divider />
|
||||
<div className="channelTabs-keybind-settings">
|
||||
<Heading>Keyboard Shortcuts</Heading>
|
||||
<Paragraph>
|
||||
Click a button and press your desired key combination. Modifiers like CTRL, SHIFT, and ALT are supported.
|
||||
</Paragraph>
|
||||
|
||||
<KeybindInput
|
||||
label="Close Tab"
|
||||
description="Close the currently active tab"
|
||||
settingKey="closeTabKeybind"
|
||||
enabledKey="enableCloseTabShortcut"
|
||||
/>
|
||||
|
||||
<KeybindInput
|
||||
label="New Tab"
|
||||
description="Open a new tab with the current channel"
|
||||
settingKey="newTabKeybind"
|
||||
enabledKey="enableNewTabShortcut"
|
||||
/>
|
||||
|
||||
<KeybindInput
|
||||
label="Cycle Tabs Forward"
|
||||
description="Switch to the next tab (wraps around to first)"
|
||||
settingKey="cycleTabForwardKeybind"
|
||||
enabledKey="enableTabCycleShortcut"
|
||||
/>
|
||||
|
||||
<KeybindInput
|
||||
label="Cycle Tabs Backward"
|
||||
description="Switch to the previous tab (wraps around to last)"
|
||||
settingKey="cycleTabBackwardKeybind"
|
||||
enabledKey="enableTabCycleShortcut"
|
||||
/>
|
||||
|
||||
<div style={{ marginTop: "16px" }}>
|
||||
<Button
|
||||
color={Button.Colors.RED}
|
||||
onClick={handleResetAll}
|
||||
>
|
||||
Reset All to Defaults
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
|
@ -20,8 +20,8 @@ import * as ChannelTabsUtils from "./util";
|
|||
|
||||
const contextMenuPatch: NavContextMenuPatchCallback = (children, props: { channel: Channel, messageId?: string; }) => {
|
||||
const { channel, messageId } = props;
|
||||
const group = findGroupChildrenByChildId("channel-copy-link", children);
|
||||
group?.push(
|
||||
|
||||
const menuItem = (
|
||||
<Menu.MenuItem
|
||||
label="Open in New Tab"
|
||||
id="open-link-in-tab"
|
||||
|
|
@ -31,6 +31,17 @@ const contextMenuPatch: NavContextMenuPatchCallback = (children, props: { channe
|
|||
}, settings.store.openInNewTabAutoSwitch, messageId, true, true)} // The true values are important for bypassing tab limits
|
||||
/>
|
||||
);
|
||||
|
||||
const group = findGroupChildrenByChildId("channel-copy-link", children);
|
||||
if (group) {
|
||||
group.push(menuItem);
|
||||
} else {
|
||||
children.splice(-1, 0, (
|
||||
<Menu.MenuGroup>
|
||||
{menuItem}
|
||||
</Menu.MenuGroup>
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
|
|
@ -40,7 +51,9 @@ export default definePlugin({
|
|||
dependencies: ["ContextMenuAPI"],
|
||||
contextMenus: {
|
||||
"channel-mention-context": contextMenuPatch,
|
||||
"channel-context": contextMenuPatch
|
||||
"channel-context": contextMenuPatch,
|
||||
"user-context": contextMenuPatch,
|
||||
"gdm-context": contextMenuPatch
|
||||
},
|
||||
patches: [
|
||||
// add the channel tab container at the top
|
||||
|
|
@ -95,12 +108,30 @@ export default definePlugin({
|
|||
|
||||
settings,
|
||||
|
||||
start() {
|
||||
// migrate old settings to new granular keybind settings
|
||||
const store = settings.store as any;
|
||||
if (store.enableHotkeys !== undefined) {
|
||||
const oldValue = store.enableHotkeys;
|
||||
settings.store.enableNumberKeySwitching = oldValue;
|
||||
settings.store.enableCloseTabShortcut = oldValue;
|
||||
settings.store.enableNewTabShortcut = oldValue;
|
||||
settings.store.enableTabCycleShortcut = oldValue;
|
||||
delete store.enableHotkeys;
|
||||
}
|
||||
if (store.hotkeyCount !== undefined) {
|
||||
settings.store.numberKeySwitchCount = store.hotkeyCount;
|
||||
delete store.hotkeyCount;
|
||||
}
|
||||
},
|
||||
|
||||
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()) {
|
||||
ChannelTabsUtils.clearNavigationFlag();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
.vc-channeltabs-tab-container,
|
||||
.vc-channeltabs-bookmark-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
|
@ -95,6 +95,22 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
/* tab scroller for horizontal overflow */
|
||||
.vc-channeltabs-tab-scroller {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.vc-channeltabs-tab-scroller::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vc-channeltabs-bookmarks {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
|
|
@ -183,6 +199,7 @@
|
|||
padding: 6px 8px;
|
||||
position: relative;
|
||||
cursor: grab;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.vc-channeltabs-tab:active {
|
||||
|
|
@ -341,6 +358,7 @@
|
|||
border-radius: 4px;
|
||||
background-color: var(--background-secondary);
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.vc-channeltabs-new-button:hover {
|
||||
|
|
@ -587,7 +605,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Quests active gradient (blue+pink) when quests are running */
|
||||
/* quests active gradient when quests are running */
|
||||
.vc-channeltabs-tab-quests-active {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
|
|
@ -601,9 +619,9 @@
|
|||
background: linear-gradient(135deg,
|
||||
transparent,
|
||||
transparent 25%,
|
||||
rgb(0 200 255) 40%,
|
||||
rgb(114 137 218) 40%,
|
||||
rgb(88 101 242) 50%,
|
||||
rgb(220 104 255) 60%,
|
||||
rgb(71 82 196) 60%,
|
||||
transparent 75%,
|
||||
transparent);
|
||||
background-size: 300% 300%;
|
||||
|
|
@ -644,7 +662,7 @@
|
|||
|
||||
@keyframes vc-channeltabs-quests-glow {
|
||||
0% {
|
||||
box-shadow: 0 0 8px 1px rgb(0 200 255 / 60%);
|
||||
box-shadow: 0 0 8px 1px rgb(114 137 218 / 60%);
|
||||
}
|
||||
|
||||
33% {
|
||||
|
|
@ -652,11 +670,11 @@
|
|||
}
|
||||
|
||||
66% {
|
||||
box-shadow: 0 0 8px 1px rgb(220 104 255 / 60%);
|
||||
box-shadow: 0 0 8px 1px rgb(71 82 196 / 60%);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 8px 1px rgb(0 200 255 / 60%);
|
||||
box-shadow: 0 0 8px 1px rgb(114 137 218 / 60%);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -672,7 +690,7 @@
|
|||
|
||||
@keyframes vc-channeltabs-quests-glow-selected {
|
||||
0% {
|
||||
box-shadow: 0 0 12px 2px rgb(0 200 255 / 80%);
|
||||
box-shadow: 0 0 12px 2px rgb(114 137 218 / 80%);
|
||||
}
|
||||
|
||||
33% {
|
||||
|
|
@ -680,11 +698,11 @@
|
|||
}
|
||||
|
||||
66% {
|
||||
box-shadow: 0 0 12px 2px rgb(220 104 255 / 80%);
|
||||
box-shadow: 0 0 12px 2px rgb(71 82 196 / 80%);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 12px 2px rgb(0 200 255 / 80%);
|
||||
box-shadow: 0 0 12px 2px rgb(114 137 218 / 80%);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1052,3 +1070,184 @@
|
|||
.vc-channeltabs-bookmark.vc-channeltabs-bookmark-active:hover {
|
||||
box-shadow: 0 0 6px var(--brand-500);
|
||||
}
|
||||
|
||||
/* keybind Settings Styles */
|
||||
.channelTabs-keybind-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.channelTabs-keybind-input {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 12px;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.channelTabs-keybind-input:hover {
|
||||
background-color: var(--background-secondary-alt);
|
||||
}
|
||||
|
||||
.channelTabs-keybind-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.channelTabs-keybind-controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.channelTabs-keybind-button {
|
||||
padding: 8px 16px;
|
||||
background-color: var(--background-tertiary);
|
||||
border: 2px solid var(--input-border);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
min-width: 140px;
|
||||
text-align: center;
|
||||
font-family: var(--font-code);
|
||||
font-weight: 600;
|
||||
color: var(--header-primary);
|
||||
transition: all 0.2s ease;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 1px var(--background-modifier-accent);
|
||||
}
|
||||
|
||||
.channelTabs-keybind-button:hover:not(:disabled) {
|
||||
background-color: var(--background-modifier-hover);
|
||||
border-color: var(--brand-experiment);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 20%);
|
||||
}
|
||||
|
||||
.channelTabs-keybind-button:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 20%);
|
||||
}
|
||||
|
||||
.channelTabs-keybind-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.channelTabs-keybind-button.listening {
|
||||
background: linear-gradient(135deg, var(--brand-experiment) 0%, var(--brand-experiment-560) 100%);
|
||||
color: var(--white);
|
||||
border-color: var(--brand-experiment);
|
||||
animation: keybind-pulse 1.5s ease-in-out infinite;
|
||||
box-shadow: 0 0 12px var(--brand-experiment);
|
||||
}
|
||||
|
||||
@keyframes keybind-pulse {
|
||||
|
||||
/* stylelint-disable-next-line rule-empty-line-before */
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.85;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.channelTabs-keybind-conflict {
|
||||
color: var(--text-danger);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-top: 2px;
|
||||
animation: shake 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
|
||||
/* stylelint-disable-next-line rule-empty-line-before */
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Tab Number Badge Styles */
|
||||
.vc-channeltabs-tab-number-badge {
|
||||
background-color: rgb(0 0 0 / 25%);
|
||||
color: var(--text-normal);
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
transition:
|
||||
opacity 0.4s ease-out 0.2s,
|
||||
transform 0.4s ease-out 0.2s,
|
||||
margin 0.4s ease-out;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* position: Left */
|
||||
.vc-channeltabs-tab-number-badge[data-position="left"] {
|
||||
margin-right: 0.5rem;
|
||||
transition:
|
||||
opacity 0.4s ease-out,
|
||||
transform 0.4s ease-out,
|
||||
margin-right 0.4s ease-out;
|
||||
}
|
||||
|
||||
/* position: Right */
|
||||
.vc-channeltabs-tab-number-badge[data-position="right"] {
|
||||
margin-left: 0.5rem;
|
||||
transition:
|
||||
opacity 0.4s ease-out 0.2s,
|
||||
transform 0.4s ease-out 0.2s,
|
||||
margin-left 0.4s ease-out;
|
||||
}
|
||||
|
||||
/* hover effect (slight scale up) */
|
||||
.vc-channeltabs-tab:hover .vc-channeltabs-tab-number-badge {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* directional slide animations for compact mode */
|
||||
.vc-channeltabs-tab-compact:not(:hover, .vc-channeltabs-tab-selected) .vc-channeltabs-tab-number-badge[data-position="right"] {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px) scale(0.95);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vc-channeltabs-tab-compact:not(:hover, .vc-channeltabs-tab-selected) .vc-channeltabs-tab-number-badge[data-position="left"] {
|
||||
opacity: 0;
|
||||
transform: translateX(-15px) scale(0.9);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* disable transitions when compact expand animation is disabled */
|
||||
.vc-channeltabs-no-compact-animation .vc-channeltabs-tab-number-badge {
|
||||
transition: none !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { SearchableSelect, useState } from "@webpack/common";
|
|||
import { JSX } from "react";
|
||||
|
||||
import { ChannelTabsPreview } from "../components/ChannelTabsContainer";
|
||||
import { KeybindSettings } from "../components/KeybindSettings";
|
||||
|
||||
interface DynamicDropdownSettingOption {
|
||||
label: string;
|
||||
|
|
@ -137,7 +138,6 @@ export const settings = definePluginSettings({
|
|||
},
|
||||
tabSet: {
|
||||
component: ChannelTabsPreview,
|
||||
description: "Select which tabs to open at startup",
|
||||
type: OptionType.COMPONENT,
|
||||
default: {}
|
||||
},
|
||||
|
|
@ -214,19 +214,81 @@ export const settings = definePluginSettings({
|
|||
],
|
||||
restartNeeded: true
|
||||
},
|
||||
enableHotkeys: {
|
||||
enableNumberKeySwitching: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable hotkey (1-9) for tab switching",
|
||||
description: "Enable number keys (1-9) to switch tabs",
|
||||
default: true,
|
||||
restartNeeded: false
|
||||
},
|
||||
hotkeyCount: {
|
||||
numberKeySwitchCount: {
|
||||
type: OptionType.SLIDER,
|
||||
description: "Number of tabs accessible via keyboard shortcuts",
|
||||
description: "Number of tabs accessible via number keys (1-9)",
|
||||
markers: makeRange(1, 9, 1),
|
||||
default: 3,
|
||||
stickToMarkers: true,
|
||||
},
|
||||
enableCloseTabShortcut: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable close tab keyboard shortcut",
|
||||
default: true,
|
||||
restartNeeded: false
|
||||
},
|
||||
enableNewTabShortcut: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable new tab keyboard shortcut",
|
||||
default: true,
|
||||
restartNeeded: false
|
||||
},
|
||||
enableTabCycleShortcut: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable tab cycling keyboard shortcut",
|
||||
default: true,
|
||||
restartNeeded: false
|
||||
},
|
||||
keybindsSection: {
|
||||
type: OptionType.COMPONENT,
|
||||
component: KeybindSettings
|
||||
},
|
||||
// me when storage yes for keybinds
|
||||
closeTabKeybind: {
|
||||
type: OptionType.STRING,
|
||||
description: "Keyboard shortcut to close the current tab",
|
||||
default: "CTRL+W",
|
||||
hidden: true
|
||||
},
|
||||
newTabKeybind: {
|
||||
type: OptionType.STRING,
|
||||
description: "Keyboard shortcut to open a new tab",
|
||||
default: "CTRL+T",
|
||||
hidden: true
|
||||
},
|
||||
cycleTabForwardKeybind: {
|
||||
type: OptionType.STRING,
|
||||
description: "Keyboard shortcut to cycle to the next tab",
|
||||
default: "CTRL+TAB",
|
||||
hidden: true
|
||||
},
|
||||
cycleTabBackwardKeybind: {
|
||||
type: OptionType.STRING,
|
||||
description: "Keyboard shortcut to cycle to the previous tab",
|
||||
default: "CTRL+SHIFT+TAB",
|
||||
hidden: true
|
||||
},
|
||||
showTabNumbers: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show numbered badges on tabs to indicate keyboard shortcuts",
|
||||
default: false,
|
||||
restartNeeded: false
|
||||
},
|
||||
tabNumberPosition: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Where to display the numbered badge on tabs",
|
||||
options: [
|
||||
{ label: "Left side (before icon)", value: "left", default: true },
|
||||
{ label: "Right side (after content)", value: "right" }
|
||||
],
|
||||
restartNeeded: false
|
||||
},
|
||||
animations: {
|
||||
type: OptionType.COMPONENT,
|
||||
component: AnimationSettings
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { DataStore } from "@api/index";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { NavigationRouter, SelectedChannelStore, SelectedGuildStore, showToast, Toasts, useState } from "@webpack/common";
|
||||
import { NavigationRouter, SelectedChannelStore, SelectedGuildStore, showToast, Toasts, useEffect, useRef, useState } from "@webpack/common";
|
||||
import { JSX } from "react";
|
||||
|
||||
import { logger, settings } from "./constants";
|
||||
|
|
@ -25,6 +25,8 @@ interface NavigationContext {
|
|||
let lastNavigationContext: NavigationContext | null = null;
|
||||
let isViewingViaBookmark = false;
|
||||
let isNavigatingViaTabFlag = false;
|
||||
let navigationTimeoutId: NodeJS.Timeout | undefined;
|
||||
const NAVIGATION_TIMEOUT_MS = 1000;
|
||||
|
||||
export function isViewingViaBookmarkMode() {
|
||||
return settings.store.bookmarksIndependentFromTabs && isViewingViaBookmark;
|
||||
|
|
@ -34,6 +36,11 @@ export function isNavigatingViaTab() {
|
|||
return isNavigatingViaTabFlag;
|
||||
}
|
||||
|
||||
export function clearNavigationFlag() {
|
||||
clearTimeout(navigationTimeoutId);
|
||||
isNavigatingViaTabFlag = false;
|
||||
}
|
||||
|
||||
export function clearBookmarkViewingMode() {
|
||||
isViewingViaBookmark = false;
|
||||
}
|
||||
|
|
@ -94,6 +101,8 @@ interface TabStateCache {
|
|||
timestamp: number;
|
||||
}
|
||||
const tabStateCache = new Map<number, TabStateCache>();
|
||||
const MAX_CACHE_SIZE = 50;
|
||||
const CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
// horror
|
||||
const _ = {
|
||||
|
|
@ -170,22 +179,31 @@ export function closeTab(id: number) {
|
|||
|
||||
if (id === currentlyOpenTab) {
|
||||
if (openTabHistory.length) {
|
||||
// use tab history to find most recently used tab
|
||||
openTabHistory.pop();
|
||||
let newTab: ChannelTabsProps | undefined = undefined;
|
||||
while (!newTab) {
|
||||
const maybeNewTabId = openTabHistory.at(-1);
|
||||
openTabHistory.pop();
|
||||
if (!maybeNewTabId) {
|
||||
moveToTab(openTabs[Math.max(i - 1, 0)].id);
|
||||
// fallback: go to tab on the right, or leftmost if closing last tab
|
||||
const fallbackIndex = i < openTabs.length ? i : 0;
|
||||
moveToTab(openTabs[fallbackIndex].id);
|
||||
break;
|
||||
}
|
||||
const maybeNewTab = openTabs.find(t => t.id === maybeNewTabId);
|
||||
if (maybeNewTab) newTab = maybeNewTab;
|
||||
}
|
||||
|
||||
moveToTab(newTab.id);
|
||||
openTabHistory.pop();
|
||||
if (newTab) {
|
||||
moveToTab(newTab.id);
|
||||
openTabHistory.pop();
|
||||
}
|
||||
} else {
|
||||
// no history: go to tab on the right, or leftmost if closing last tab
|
||||
const fallbackIndex = i < openTabs.length ? i : 0;
|
||||
moveToTab(openTabs[fallbackIndex].id);
|
||||
}
|
||||
else moveToTab(openTabs[Math.max(i - 1, 0)].id);
|
||||
}
|
||||
if (i !== openTabs.length) bumpGhostTabCount();
|
||||
else clearGhostTabs();
|
||||
|
|
@ -319,11 +337,33 @@ function getScrollContainer(): HTMLElement | null {
|
|||
return document.querySelector('[class*="scrollerInner_"]') as HTMLElement;
|
||||
}
|
||||
|
||||
function evictStaleCache() {
|
||||
const now = Date.now();
|
||||
|
||||
for (const [tabId, cache] of tabStateCache.entries()) {
|
||||
if (now - cache.timestamp > CACHE_TTL_MS) {
|
||||
tabStateCache.delete(tabId);
|
||||
}
|
||||
}
|
||||
|
||||
if (tabStateCache.size > MAX_CACHE_SIZE) {
|
||||
const entries = Array.from(tabStateCache.entries())
|
||||
.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
||||
|
||||
const entriesToRemove = entries.slice(0, tabStateCache.size - MAX_CACHE_SIZE);
|
||||
for (const [tabId] of entriesToRemove) {
|
||||
tabStateCache.delete(tabId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cacheCurrentTabState() {
|
||||
if (!settings.store.renderAllTabs) return;
|
||||
|
||||
const scrollContainer = getScrollContainer();
|
||||
if (scrollContainer && currentlyOpenTab !== undefined) {
|
||||
evictStaleCache();
|
||||
|
||||
tabStateCache.set(currentlyOpenTab, {
|
||||
scrollPosition: scrollContainer.scrollTop,
|
||||
timestamp: Date.now()
|
||||
|
|
@ -361,6 +401,7 @@ export function moveToTab(id: number) {
|
|||
setOpenTab(id);
|
||||
|
||||
// SET FLAG: We're initiating navigation
|
||||
clearTimeout(navigationTimeoutId);
|
||||
isNavigatingViaTabFlag = true;
|
||||
|
||||
// handle special pages with synthetic channelIds
|
||||
|
|
@ -381,8 +422,10 @@ export function moveToTab(id: number) {
|
|||
if (route) {
|
||||
setNavigationSource(tab.guildId, tab.channelId, "tab");
|
||||
NavigationRouter.transitionTo(route);
|
||||
// Clear flag after navigation completes
|
||||
setTimeout(() => { isNavigatingViaTabFlag = false; }, 100);
|
||||
// Clear flag after navigation with safety timeout
|
||||
navigationTimeoutId = setTimeout(() => {
|
||||
isNavigatingViaTabFlag = false;
|
||||
}, NAVIGATION_TIMEOUT_MS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -401,8 +444,10 @@ export function moveToTab(id: number) {
|
|||
}
|
||||
else update();
|
||||
|
||||
// Clear flag after navigation
|
||||
setTimeout(() => { isNavigatingViaTabFlag = false; }, 100);
|
||||
// Clear flag after navigation with safety timeout
|
||||
navigationTimeoutId = setTimeout(() => {
|
||||
isNavigatingViaTabFlag = false;
|
||||
}, NAVIGATION_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
export function openStartupTabs(props: BasicChannelTabsProps & { userId: string; }, setUserId: (id: string) => void) {
|
||||
|
|
@ -417,19 +462,26 @@ export function openStartupTabs(props: BasicChannelTabsProps & { userId: string;
|
|||
|
||||
switch (settings.store.onStartup) {
|
||||
case "remember": {
|
||||
persistedTabs.then(tabs => {
|
||||
const t = tabs?.[userId];
|
||||
if (!t) {
|
||||
createTab({ channelId: props.channelId, guildId: props.guildId }, true);
|
||||
return showToast("Failed to restore tabs", Toasts.Type.FAILURE);
|
||||
}
|
||||
replaceArray(openTabs); // empty the array
|
||||
t.openTabs.forEach(tab => createTab(tab));
|
||||
currentlyOpenTab = openTabs[t.openTabIndex]?.id ?? 0;
|
||||
persistedTabs
|
||||
.then(tabs => {
|
||||
const t = tabs?.[userId];
|
||||
if (!t) {
|
||||
createTab({ channelId: props.channelId, guildId: props.guildId }, true);
|
||||
return showToast("Failed to restore tabs", Toasts.Type.FAILURE);
|
||||
}
|
||||
replaceArray(openTabs); // empty the array
|
||||
t.openTabs.forEach(tab => createTab(tab));
|
||||
currentlyOpenTab = openTabs[t.openTabIndex]?.id ?? 0;
|
||||
|
||||
setUserId(userId);
|
||||
moveToTab(currentlyOpenTab);
|
||||
});
|
||||
setUserId(userId);
|
||||
moveToTab(currentlyOpenTab);
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error("Failed to load persisted tabs from DataStore", error);
|
||||
showToast("Failed to load saved tabs", Toasts.Type.FAILURE);
|
||||
createTab({ channelId: props.channelId, guildId: props.guildId }, true);
|
||||
setUserId(userId);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "preset": {
|
||||
|
|
@ -540,18 +592,28 @@ export function toggleCompactTab(id: number) {
|
|||
}
|
||||
|
||||
export function useGhostTabs() {
|
||||
let timeout;
|
||||
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
bumpGhostTabCount = () => {
|
||||
setCount(count + 1);
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
setCount(prev => prev + 1);
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setCount(0);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
clearGhostTabs = () => {
|
||||
clearTimeout(timeout);
|
||||
clearTimeout(timeoutRef.current);
|
||||
setCount(0);
|
||||
};
|
||||
|
||||
// cleanup timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimeout(timeoutRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return new Array<JSX.Element>(count).fill(<div className={cl("tab", "ghost-tab")} />);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue