mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-28 16:04:24 -04:00
Revert "moved accelerator dir to equicordplugins dir"
This reverts commit 95dc23b6a2
.
This commit is contained in:
parent
40e07392e1
commit
acfa10992f
9 changed files with 0 additions and 2004 deletions
|
@ -1,237 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccessories";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
import { channelPreloader } from "./modules/preloader";
|
||||
import { fastCache } from "./modules/fastcache";
|
||||
import { intersectionOptimizer } from "./modules/intersection";
|
||||
import { imagePreloader } from "./modules/imagepreloader";
|
||||
import { CacheIndicatorAccessory } from "./modules/messageAccessory";
|
||||
import { statsTracker, FloatingStats } from "./modules/stats";
|
||||
|
||||
const logger = new Logger("Accelerator");
|
||||
|
||||
const settings = definePluginSettings({
|
||||
enablePreloading: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Preload adjacent channels and recent DMs for instant switching",
|
||||
default: true,
|
||||
restartNeeded: true
|
||||
},
|
||||
|
||||
enableFastCache: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Thread-safe high-performance message and user caching",
|
||||
default: true,
|
||||
restartNeeded: true
|
||||
},
|
||||
|
||||
enableImagePreloading: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Intelligent image preloading for smoother scrolling",
|
||||
default: true,
|
||||
restartNeeded: true
|
||||
},
|
||||
|
||||
enableViewportOptimization: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Use intersection observers for efficient rendering",
|
||||
default: true,
|
||||
restartNeeded: true
|
||||
},
|
||||
|
||||
showStatsWindow: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show floating performance statistics window",
|
||||
default: true,
|
||||
restartNeeded: true
|
||||
},
|
||||
|
||||
showCacheIndicators: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show green dots on messages served instantly from cache",
|
||||
default: true,
|
||||
restartNeeded: true
|
||||
},
|
||||
|
||||
preloadDistance: {
|
||||
type: OptionType.SLIDER,
|
||||
description: "How many adjacent channels to preload",
|
||||
default: 3,
|
||||
markers: [1, 2, 3, 4, 5],
|
||||
stickToMarkers: true,
|
||||
restartNeeded: true
|
||||
},
|
||||
|
||||
cacheSize: {
|
||||
type: OptionType.SLIDER,
|
||||
description: "Cache size in MB (uses thread-safe buckets)",
|
||||
default: 256,
|
||||
markers: [128, 256, 384, 512, 640, 768, 896, 1024],
|
||||
stickToMarkers: true,
|
||||
restartNeeded: true
|
||||
},
|
||||
|
||||
maxImagePreload: {
|
||||
type: OptionType.SLIDER,
|
||||
description: "Maximum images to preload ahead",
|
||||
default: 10,
|
||||
markers: [5, 10, 20, 50],
|
||||
stickToMarkers: true,
|
||||
restartNeeded: true
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "Accelerator",
|
||||
description: "High-performance Discord optimization using thread-safe caching, intelligent preloading, and zero CSS interference",
|
||||
authors: [EquicordDevs.galpt],
|
||||
tags: ["performance", "optimization", "preload", "cache", "thread-safe"],
|
||||
dependencies: ["MessageAccessoriesAPI"],
|
||||
|
||||
settings,
|
||||
|
||||
// Minimal patches - only for tracking performance, no CSS modifications
|
||||
patches: [
|
||||
{
|
||||
find: "CONNECTION_OPEN:",
|
||||
replacement: {
|
||||
match: /(CONNECTION_OPEN:function\(\w+\)\{)/,
|
||||
replace: "$1/* Accelerator: Performance tracking */"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
flux: {
|
||||
// Channel switching with immediate preloading
|
||||
CHANNEL_SELECT({ channelId, guildId }) {
|
||||
if (channelId) {
|
||||
statsTracker.trackChannelSwitchStart(channelId);
|
||||
|
||||
if (settings.store.enablePreloading) {
|
||||
channelPreloader.preloadAdjacent(guildId, channelId, settings.store.preloadDistance);
|
||||
}
|
||||
|
||||
if (settings.store.enableImagePreloading) {
|
||||
imagePreloader.preloadChannelImages(channelId, settings.store.maxImagePreload);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Track message loading and fast cache integration
|
||||
LOAD_MESSAGES_SUCCESS({ channelId, messages }) {
|
||||
statsTracker.trackChannelSwitchEnd(channelId);
|
||||
|
||||
// Always track messages loaded for statistics
|
||||
if (messages?.length) {
|
||||
statsTracker.incrementMessagesLoaded(messages.length);
|
||||
}
|
||||
|
||||
// Add to fast cache if enabled
|
||||
if (settings.store.enableFastCache && messages?.length) {
|
||||
fastCache.addMessageBatch(channelId, messages);
|
||||
}
|
||||
},
|
||||
|
||||
// Cache new messages atomically and track them
|
||||
MESSAGE_CREATE({ message }) {
|
||||
if (message) {
|
||||
// Add to fast cache if enabled and track cached count
|
||||
if (settings.store.enableFastCache) {
|
||||
fastCache.addMessage(message.channel_id, message);
|
||||
statsTracker.incrementMessagesCached(1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Track cache performance
|
||||
LOAD_MESSAGES_START({ channelId }) {
|
||||
if (settings.store.enableFastCache) {
|
||||
const cached = fastCache.getMessages(channelId);
|
||||
if (cached.length > 0) {
|
||||
statsTracker.incrementCacheHit();
|
||||
logger.debug(`Found ${cached.length} cached messages for channel ${channelId}`);
|
||||
} else {
|
||||
statsTracker.incrementCacheMiss();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// User data caching for profile optimization
|
||||
USER_UPDATE({ user }) {
|
||||
if (settings.store.enableFastCache && user) {
|
||||
fastCache.addUser(user.id, user);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async start() {
|
||||
// Initialize performance tracking
|
||||
statsTracker.init();
|
||||
|
||||
// Initialize thread-safe cache system first
|
||||
if (settings.store.enableFastCache) {
|
||||
await fastCache.init(settings.store.cacheSize * 1024 * 1024); // Convert MB to bytes
|
||||
}
|
||||
|
||||
// Initialize pure JavaScript optimizations
|
||||
if (settings.store.enableViewportOptimization) {
|
||||
intersectionOptimizer.init();
|
||||
}
|
||||
|
||||
if (settings.store.enablePreloading) {
|
||||
channelPreloader.init(settings.store.preloadDistance);
|
||||
}
|
||||
|
||||
if (settings.store.enableImagePreloading) {
|
||||
imagePreloader.init(settings.store.maxImagePreload);
|
||||
}
|
||||
|
||||
// Initialize message accessories for cache indicators
|
||||
if (settings.store.enableFastCache && settings.store.showCacheIndicators) {
|
||||
addMessageAccessory("accelerator-cache-indicator", props =>
|
||||
CacheIndicatorAccessory({ message: props.message })
|
||||
);
|
||||
}
|
||||
|
||||
// Show stats window (this is the ONLY UI/CSS component)
|
||||
if (settings.store.showStatsWindow) {
|
||||
FloatingStats.show();
|
||||
}
|
||||
|
||||
// Set up periodic cache stats sync
|
||||
if (settings.store.enableFastCache) {
|
||||
setInterval(() => {
|
||||
const totalCached = fastCache.getTotalMessagesCached();
|
||||
if (totalCached !== statsTracker.getStats().messagesCached) {
|
||||
statsTracker.updateCacheStats({
|
||||
totalMessagesCached: totalCached
|
||||
});
|
||||
}
|
||||
}, 5000); // Sync every 5 seconds
|
||||
}
|
||||
},
|
||||
|
||||
stop() {
|
||||
// Clean shutdown of all systems
|
||||
channelPreloader.cleanup();
|
||||
fastCache.cleanup();
|
||||
intersectionOptimizer.cleanup();
|
||||
imagePreloader.cleanup();
|
||||
statsTracker.cleanup();
|
||||
FloatingStats.hide();
|
||||
|
||||
// Remove message accessories
|
||||
if (settings.store.enableFastCache && settings.store.showCacheIndicators) {
|
||||
removeMessageAccessory("accelerator-cache-indicator");
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,351 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { statsTracker } from "./stats";
|
||||
|
||||
const logger = new Logger("Accelerator:FastCache");
|
||||
|
||||
// Inspired by VictoriaMetrics/fastcache - thread-safe cache with buckets
|
||||
// Each bucket has its own lock (simulated with async operations) to reduce contention
|
||||
|
||||
interface CacheEntry {
|
||||
key: string;
|
||||
value: any;
|
||||
timestamp: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface Bucket {
|
||||
entries: Map<string, CacheEntry>;
|
||||
totalSize: number;
|
||||
lastCleanup: number;
|
||||
}
|
||||
|
||||
class FastCache {
|
||||
private buckets: Bucket[] = [];
|
||||
private bucketCount = 256; // Power of 2 for fast modulo
|
||||
private maxCacheSize = 256 * 1024 * 1024; // 256 MB default
|
||||
private maxEntryAge = 30 * 60 * 1000; // 30 minutes
|
||||
private cleanupInterval: any | null = null;
|
||||
private stats = {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
evictions: 0,
|
||||
totalEntries: 0
|
||||
};
|
||||
|
||||
async init(maxSizeBytes: number): Promise<void> {
|
||||
this.maxCacheSize = maxSizeBytes;
|
||||
this.buckets = [];
|
||||
|
||||
// Initialize buckets (like chunks in fastcache)
|
||||
for (let i = 0; i < this.bucketCount; i++) {
|
||||
this.buckets.push({
|
||||
entries: new Map(),
|
||||
totalSize: 0,
|
||||
lastCleanup: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
// Start background cleanup (like GC in fastcache)
|
||||
this.cleanupInterval = setInterval(() => {
|
||||
this.performCleanup();
|
||||
}, 60 * 1000); // Every minute
|
||||
|
||||
logger.info(`FastCache initialized with ${this.bucketCount} buckets, max size: ${Math.round(maxSizeBytes / 1024 / 1024)}MB`);
|
||||
}
|
||||
|
||||
// Hash function to distribute keys across buckets
|
||||
private hash(key: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < key.length; i++) {
|
||||
const char = key.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
return Math.abs(hash) % this.bucketCount;
|
||||
}
|
||||
|
||||
// Estimate size of an object (simplified)
|
||||
private estimateSize(obj: any): number {
|
||||
if (obj === null || obj === undefined) return 8;
|
||||
if (typeof obj === "string") return obj.length * 2; // UTF-16
|
||||
if (typeof obj === "number") return 8;
|
||||
if (typeof obj === "boolean") return 4;
|
||||
if (Array.isArray(obj)) {
|
||||
return 24 + obj.reduce((acc, item) => acc + this.estimateSize(item), 0);
|
||||
}
|
||||
if (typeof obj === "object") {
|
||||
return 24 + Object.keys(obj).reduce((acc, key) => {
|
||||
return acc + this.estimateSize(key) + this.estimateSize(obj[key]);
|
||||
}, 0);
|
||||
}
|
||||
return 24; // Default object overhead
|
||||
}
|
||||
|
||||
// Thread-safe set operation
|
||||
set(key: string, value: any): void {
|
||||
const bucketIndex = this.hash(key);
|
||||
const bucket = this.buckets[bucketIndex];
|
||||
const size = this.estimateSize(value);
|
||||
const now = Date.now();
|
||||
|
||||
// Remove old entry if exists
|
||||
const existing = bucket.entries.get(key);
|
||||
if (existing) {
|
||||
bucket.totalSize -= existing.size;
|
||||
this.stats.totalEntries--;
|
||||
}
|
||||
|
||||
// Add new entry
|
||||
const entry: CacheEntry = {
|
||||
key,
|
||||
value,
|
||||
timestamp: now,
|
||||
size
|
||||
};
|
||||
|
||||
bucket.entries.set(key, entry);
|
||||
bucket.totalSize += size;
|
||||
this.stats.totalEntries++;
|
||||
|
||||
// Evict if bucket is too large
|
||||
this.evictFromBucketIfNeeded(bucket);
|
||||
}
|
||||
|
||||
// Thread-safe get operation
|
||||
get(key: string): any | null {
|
||||
const bucketIndex = this.hash(key);
|
||||
const bucket = this.buckets[bucketIndex];
|
||||
const entry = bucket.entries.get(key);
|
||||
|
||||
if (!entry) {
|
||||
this.stats.misses++;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if entry is still fresh
|
||||
const now = Date.now();
|
||||
if (now - entry.timestamp > this.maxEntryAge) {
|
||||
bucket.entries.delete(key);
|
||||
bucket.totalSize -= entry.size;
|
||||
this.stats.totalEntries--;
|
||||
this.stats.evictions++;
|
||||
this.stats.misses++;
|
||||
return null;
|
||||
}
|
||||
|
||||
this.stats.hits++;
|
||||
return entry.value;
|
||||
}
|
||||
|
||||
// Message-specific operations
|
||||
addMessage(channelId: string, message: any): void {
|
||||
if (!message?.id) return;
|
||||
const key = `msg:${channelId}:${message.id}`;
|
||||
this.set(key, message);
|
||||
}
|
||||
|
||||
addMessageBatch(channelId: string, messages: any[]): void {
|
||||
for (const message of messages) {
|
||||
this.addMessage(channelId, message);
|
||||
}
|
||||
}
|
||||
|
||||
getMessage(channelId: string, messageId: string): any | null {
|
||||
const key = `msg:${channelId}:${messageId}`;
|
||||
return this.get(key);
|
||||
}
|
||||
|
||||
getMessages(channelId: string): any[] {
|
||||
const messages: any[] = [];
|
||||
const prefix = `msg:${channelId}:`;
|
||||
|
||||
// Search across all buckets for this channel's messages
|
||||
for (const bucket of this.buckets) {
|
||||
for (const [key, entry] of bucket.entries) {
|
||||
if (key.startsWith(prefix)) {
|
||||
const now = Date.now();
|
||||
if (now - entry.timestamp <= this.maxEntryAge) {
|
||||
messages.push(entry.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return messages.sort((a, b) =>
|
||||
new Date(a.timestamp || 0).getTime() - new Date(b.timestamp || 0).getTime()
|
||||
);
|
||||
}
|
||||
|
||||
getMessageCount(channelId: string): number {
|
||||
const prefix = `msg:${channelId}:`;
|
||||
let count = 0;
|
||||
|
||||
for (const bucket of this.buckets) {
|
||||
for (const [key, entry] of bucket.entries) {
|
||||
if (key.startsWith(prefix)) {
|
||||
const now = Date.now();
|
||||
if (now - entry.timestamp <= this.maxEntryAge) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
getTotalMessagesCached(): number {
|
||||
let count = 0;
|
||||
for (const bucket of this.buckets) {
|
||||
for (const [key, entry] of bucket.entries) {
|
||||
if (key.startsWith('msg:')) {
|
||||
const now = Date.now();
|
||||
if (now - entry.timestamp <= this.maxEntryAge) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// User-specific operations
|
||||
addUser(userId: string, user: any): void {
|
||||
const key = `user:${userId}`;
|
||||
this.set(key, user);
|
||||
}
|
||||
|
||||
getUser(userId: string): any | null {
|
||||
const key = `user:${userId}`;
|
||||
return this.get(key);
|
||||
}
|
||||
|
||||
// Channel data operations
|
||||
addChannelData(channelId: string, data: any): void {
|
||||
const key = `channel:${channelId}`;
|
||||
this.set(key, data);
|
||||
}
|
||||
|
||||
getChannelData(channelId: string): any | null {
|
||||
const key = `channel:${channelId}`;
|
||||
return this.get(key);
|
||||
}
|
||||
|
||||
// Eviction policy (LRU-like but simplified for performance)
|
||||
private evictFromBucketIfNeeded(bucket: Bucket): void {
|
||||
const maxBucketSize = this.maxCacheSize / this.bucketCount;
|
||||
|
||||
while (bucket.totalSize > maxBucketSize && bucket.entries.size > 0) {
|
||||
// Find oldest entry
|
||||
let oldestKey = "";
|
||||
let oldestTime = Date.now();
|
||||
|
||||
for (const [key, entry] of bucket.entries) {
|
||||
if (entry.timestamp < oldestTime) {
|
||||
oldestTime = entry.timestamp;
|
||||
oldestKey = key;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldestKey) {
|
||||
const entry = bucket.entries.get(oldestKey);
|
||||
if (entry) {
|
||||
bucket.entries.delete(oldestKey);
|
||||
bucket.totalSize -= entry.size;
|
||||
this.stats.totalEntries--;
|
||||
this.stats.evictions++;
|
||||
}
|
||||
} else {
|
||||
break; // Safety break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Background cleanup (like fastcache's background GC)
|
||||
private performCleanup(): void {
|
||||
const now = Date.now();
|
||||
let totalCleaned = 0;
|
||||
|
||||
for (const bucket of this.buckets) {
|
||||
// Only clean buckets that haven't been cleaned recently
|
||||
if (now - bucket.lastCleanup < 30 * 1000) continue; // 30 seconds
|
||||
|
||||
const keysToDelete: string[] = [];
|
||||
|
||||
for (const [key, entry] of bucket.entries) {
|
||||
if (now - entry.timestamp > this.maxEntryAge) {
|
||||
keysToDelete.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of keysToDelete) {
|
||||
const entry = bucket.entries.get(key);
|
||||
if (entry) {
|
||||
bucket.entries.delete(key);
|
||||
bucket.totalSize -= entry.size;
|
||||
this.stats.totalEntries--;
|
||||
totalCleaned++;
|
||||
}
|
||||
}
|
||||
|
||||
bucket.lastCleanup = now;
|
||||
}
|
||||
|
||||
if (totalCleaned > 0) {
|
||||
logger.debug(`Cleanup removed ${totalCleaned} expired entries`);
|
||||
}
|
||||
|
||||
// Update stats tracker
|
||||
statsTracker.updateCacheStats({
|
||||
hits: this.stats.hits,
|
||||
misses: this.stats.misses,
|
||||
evictions: this.stats.evictions,
|
||||
totalEntries: this.stats.totalEntries,
|
||||
totalSize: this.getTotalSize()
|
||||
});
|
||||
}
|
||||
|
||||
// Get cache statistics
|
||||
getStats() {
|
||||
return {
|
||||
...this.stats,
|
||||
totalSize: this.getTotalSize(),
|
||||
bucketCount: this.bucketCount,
|
||||
avgBucketSize: this.getTotalSize() / this.bucketCount
|
||||
};
|
||||
}
|
||||
|
||||
private getTotalSize(): number {
|
||||
return this.buckets.reduce((total, bucket) => total + bucket.totalSize, 0);
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
if (this.cleanupInterval) {
|
||||
clearInterval(this.cleanupInterval);
|
||||
this.cleanupInterval = null;
|
||||
}
|
||||
|
||||
// Clear all buckets
|
||||
for (const bucket of this.buckets) {
|
||||
bucket.entries.clear();
|
||||
bucket.totalSize = 0;
|
||||
}
|
||||
|
||||
this.stats = {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
evictions: 0,
|
||||
totalEntries: 0
|
||||
};
|
||||
|
||||
logger.debug("FastCache cleanup completed");
|
||||
}
|
||||
}
|
||||
|
||||
export const fastCache = new FastCache();
|
|
@ -1,255 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { MessageStore } from "@webpack/common";
|
||||
|
||||
const logger = new Logger("Accelerator:ImagePreloader");
|
||||
|
||||
class ImagePreloader {
|
||||
private preloadedImages = new Set<string>();
|
||||
private preloadQueue: string[] = [];
|
||||
private isPreloading = false;
|
||||
private maxPreload = 10;
|
||||
private intersectionObserver: IntersectionObserver | null = null;
|
||||
|
||||
init(maxPreload: number): void {
|
||||
this.maxPreload = maxPreload;
|
||||
this.setupIntersectionObserver();
|
||||
logger.info("Image preloader initialized");
|
||||
}
|
||||
|
||||
// Set up intersection observer for intelligent preloading
|
||||
private setupIntersectionObserver(): void {
|
||||
this.intersectionObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
this.preloadNearbyImages(entry.target);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: "100px 0px 100px 0px", // Preload when 100px away
|
||||
threshold: 0.1
|
||||
}
|
||||
);
|
||||
|
||||
// Observe existing images
|
||||
this.observeExistingImages();
|
||||
|
||||
// Use MutationObserver to watch for new images
|
||||
const mutationObserver = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === "childList") {
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
this.observeImagesInElement(node as Element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
private observeExistingImages(): void {
|
||||
const images = document.querySelectorAll('img, [style*="background-image"]');
|
||||
for (const img of images) {
|
||||
this.intersectionObserver?.observe(img);
|
||||
}
|
||||
}
|
||||
|
||||
private observeImagesInElement(element: Element): void {
|
||||
// Observe the element itself if it's an image
|
||||
if (element.tagName === "IMG" ||
|
||||
(element as HTMLElement).style.backgroundImage) {
|
||||
this.intersectionObserver?.observe(element);
|
||||
}
|
||||
|
||||
// Observe child images
|
||||
const images = element.querySelectorAll('img, [style*="background-image"]');
|
||||
for (const img of images) {
|
||||
this.intersectionObserver?.observe(img);
|
||||
}
|
||||
}
|
||||
|
||||
private preloadNearbyImages(target: Element): void {
|
||||
// Find adjacent images to preload
|
||||
const container = target.closest('[class*="message"], [class*="content"]');
|
||||
if (!container) return;
|
||||
|
||||
const nearbyImages = this.findNearbyImageUrls(container);
|
||||
this.addToPreloadQueue(nearbyImages);
|
||||
}
|
||||
|
||||
private findNearbyImageUrls(container: Element): string[] {
|
||||
const urls: string[] = [];
|
||||
|
||||
// Find images in current and adjacent messages
|
||||
const messageElements = container.parentElement?.querySelectorAll('[class*="message"]') || [];
|
||||
const currentIndex = Array.from(messageElements).indexOf(container as Element);
|
||||
|
||||
// Check current message and a few before/after
|
||||
const start = Math.max(0, currentIndex - 2);
|
||||
const end = Math.min(messageElements.length, currentIndex + 3);
|
||||
|
||||
for (let i = start; i < end; i++) {
|
||||
const messageEl = messageElements[i];
|
||||
|
||||
// Find image URLs in this message
|
||||
const images = messageEl.querySelectorAll('img');
|
||||
for (const img of images) {
|
||||
if (img.src && !this.preloadedImages.has(img.src)) {
|
||||
urls.push(img.src);
|
||||
}
|
||||
}
|
||||
|
||||
// Find background images
|
||||
const elementsWithBg = messageEl.querySelectorAll('[style*="background-image"]');
|
||||
for (const el of elementsWithBg) {
|
||||
const bgImage = (el as HTMLElement).style.backgroundImage;
|
||||
const urlMatch = bgImage.match(/url\(['"]?(.*?)['"]?\)/);
|
||||
if (urlMatch && urlMatch[1] && !this.preloadedImages.has(urlMatch[1])) {
|
||||
urls.push(urlMatch[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return urls.slice(0, this.maxPreload);
|
||||
}
|
||||
|
||||
preloadChannelImages(channelId: string, maxImages: number): void {
|
||||
try {
|
||||
// Get messages from Discord's message store
|
||||
const messages = MessageStore.getMessages(channelId);
|
||||
if (!messages?._array) return;
|
||||
|
||||
const imageUrls: string[] = [];
|
||||
|
||||
// Extract image URLs from recent messages
|
||||
for (const message of messages._array.slice(-20)) { // Last 20 messages
|
||||
if (imageUrls.length >= maxImages) break;
|
||||
|
||||
// Check attachments
|
||||
if (message.attachments) {
|
||||
for (const attachment of message.attachments) {
|
||||
if (attachment.content_type?.startsWith('image/') &&
|
||||
attachment.url &&
|
||||
!this.preloadedImages.has(attachment.url)) {
|
||||
imageUrls.push(attachment.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check embeds
|
||||
if (message.embeds) {
|
||||
for (const embed of message.embeds) {
|
||||
if (embed.image?.url && !this.preloadedImages.has(embed.image.url)) {
|
||||
imageUrls.push(embed.image.url);
|
||||
}
|
||||
if (embed.thumbnail?.url && !this.preloadedImages.has(embed.thumbnail.url)) {
|
||||
imageUrls.push(embed.thumbnail.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.addToPreloadQueue(imageUrls.slice(0, maxImages));
|
||||
} catch (error) {
|
||||
logger.warn("Failed to preload channel images:", error);
|
||||
}
|
||||
}
|
||||
|
||||
private addToPreloadQueue(urls: string[]): void {
|
||||
for (const url of urls) {
|
||||
if (!this.preloadedImages.has(url) && !this.preloadQueue.includes(url)) {
|
||||
this.preloadQueue.push(url);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isPreloading) {
|
||||
this.processPreloadQueue();
|
||||
}
|
||||
}
|
||||
|
||||
private async processPreloadQueue(): Promise<void> {
|
||||
if (this.isPreloading || this.preloadQueue.length === 0) return;
|
||||
|
||||
this.isPreloading = true;
|
||||
|
||||
while (this.preloadQueue.length > 0 && this.preloadedImages.size < this.maxPreload * 2) {
|
||||
const url = this.preloadQueue.shift();
|
||||
if (!url || this.preloadedImages.has(url)) continue;
|
||||
|
||||
try {
|
||||
await this.preloadImage(url);
|
||||
this.preloadedImages.add(url);
|
||||
} catch (error) {
|
||||
logger.debug(`Failed to preload image: ${url}`, error);
|
||||
}
|
||||
|
||||
// Add small delay to avoid blocking the main thread
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
this.isPreloading = false;
|
||||
}
|
||||
|
||||
private preloadImage(url: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
|
||||
const cleanup = () => {
|
||||
img.onload = null;
|
||||
img.onerror = null;
|
||||
img.onabort = null;
|
||||
};
|
||||
|
||||
img.onload = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
cleanup();
|
||||
reject(new Error(`Failed to load ${url}`));
|
||||
};
|
||||
|
||||
img.onabort = () => {
|
||||
cleanup();
|
||||
reject(new Error(`Aborted loading ${url}`));
|
||||
};
|
||||
|
||||
// Set timeout to avoid hanging
|
||||
setTimeout(() => {
|
||||
cleanup();
|
||||
reject(new Error(`Timeout loading ${url}`));
|
||||
}, 5000);
|
||||
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
if (this.intersectionObserver) {
|
||||
this.intersectionObserver.disconnect();
|
||||
this.intersectionObserver = null;
|
||||
}
|
||||
|
||||
this.preloadedImages.clear();
|
||||
this.preloadQueue.length = 0;
|
||||
this.isPreloading = false;
|
||||
|
||||
logger.debug("Image preloader cleanup completed");
|
||||
}
|
||||
}
|
||||
|
||||
export const imagePreloader = new ImagePreloader();
|
|
@ -1,173 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { statsTracker } from "./stats";
|
||||
|
||||
const logger = new Logger("Accelerator:IntersectionOptimizer");
|
||||
|
||||
class IntersectionOptimizer {
|
||||
private imageObserver: IntersectionObserver | null = null;
|
||||
private observedElements = new Set<Element>();
|
||||
private imagePreloadQueue = new Set<string>();
|
||||
private isEnabled = false;
|
||||
|
||||
init() {
|
||||
this.setupImageObserver();
|
||||
this.isEnabled = true;
|
||||
logger.info("Intersection optimizer initialized (scroll-safe mode)");
|
||||
}
|
||||
|
||||
private setupImageObserver() {
|
||||
// Only handle image lazy loading - no layout modifications to avoid scroll issues
|
||||
this.imageObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target as HTMLImageElement;
|
||||
this.optimizeImageLoading(img);
|
||||
this.imageObserver?.unobserve(img);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
rootMargin: '1000px 0px', // Preload images 1000px before they enter viewport
|
||||
threshold: 0.01
|
||||
});
|
||||
|
||||
this.observeExistingImages();
|
||||
this.setupImageMutationObserver();
|
||||
}
|
||||
|
||||
private observeExistingImages() {
|
||||
if (!this.imageObserver) return;
|
||||
|
||||
// Only observe Discord CDN images to avoid interfering with other content
|
||||
const images = document.querySelectorAll('img[src*="cdn.discordapp.com"], img[src*="media.discordapp.net"]');
|
||||
images.forEach(img => {
|
||||
if (!this.observedElements.has(img)) {
|
||||
this.imageObserver!.observe(img);
|
||||
this.observedElements.add(img);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setupImageMutationObserver() {
|
||||
if (!this.imageObserver) return;
|
||||
|
||||
const mutationObserver = new MutationObserver(mutations => {
|
||||
if (!this.isEnabled) return;
|
||||
|
||||
mutations.forEach(mutation => {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (node instanceof HTMLElement) {
|
||||
// Check for new Discord images only
|
||||
const images = node.matches('img') ? [node] :
|
||||
Array.from(node.querySelectorAll('img[src*="cdn.discordapp.com"], img[src*="media.discordapp.net"]'));
|
||||
|
||||
images.forEach(img => {
|
||||
if (!this.observedElements.has(img) && this.imageObserver) {
|
||||
this.imageObserver.observe(img);
|
||||
this.observedElements.add(img);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
mutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
private optimizeImageLoading(img: HTMLImageElement) {
|
||||
try {
|
||||
// Only apply safe image optimizations that don't affect layout
|
||||
if (!img.loading) {
|
||||
img.loading = 'lazy';
|
||||
}
|
||||
if (!img.decoding) {
|
||||
img.decoding = 'async';
|
||||
}
|
||||
|
||||
// Preload higher quality version if available
|
||||
const src = img.src;
|
||||
if (src && (src.includes('cdn.discordapp.com') || src.includes('media.discordapp.net'))) {
|
||||
this.preloadHigherQuality(src);
|
||||
}
|
||||
|
||||
logger.debug(`Optimized image loading: ${img.src}`);
|
||||
statsTracker.incrementImagesOptimized();
|
||||
} catch (error) {
|
||||
logger.error("Failed to optimize image loading:", error);
|
||||
}
|
||||
}
|
||||
|
||||
private preloadHigherQuality(src: string) {
|
||||
if (this.imagePreloadQueue.has(src)) return;
|
||||
|
||||
this.imagePreloadQueue.add(src);
|
||||
|
||||
try {
|
||||
// Convert to higher quality if it's a Discord CDN image with quality params
|
||||
let highQualitySrc = src;
|
||||
|
||||
if (src.includes('?')) {
|
||||
const url = new URL(src);
|
||||
|
||||
// Remove width/height constraints for better quality
|
||||
url.searchParams.delete('width');
|
||||
url.searchParams.delete('height');
|
||||
|
||||
// Set higher quality if quality param exists
|
||||
if (url.searchParams.has('quality')) {
|
||||
url.searchParams.set('quality', '100');
|
||||
}
|
||||
|
||||
// Remove format constraints to get original format
|
||||
if (url.searchParams.has('format')) {
|
||||
url.searchParams.delete('format');
|
||||
}
|
||||
|
||||
highQualitySrc = url.toString();
|
||||
}
|
||||
|
||||
// Preload the higher quality version
|
||||
if (highQualitySrc !== src) {
|
||||
const preloadImg = new Image();
|
||||
preloadImg.onload = () => {
|
||||
this.imagePreloadQueue.delete(src);
|
||||
logger.debug(`Preloaded high quality image: ${highQualitySrc}`);
|
||||
};
|
||||
preloadImg.onerror = () => {
|
||||
this.imagePreloadQueue.delete(src);
|
||||
};
|
||||
preloadImg.src = highQualitySrc;
|
||||
} else {
|
||||
this.imagePreloadQueue.delete(src);
|
||||
}
|
||||
} catch (error) {
|
||||
this.imagePreloadQueue.delete(src);
|
||||
logger.error("Failed to preload higher quality image:", error);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.isEnabled = false;
|
||||
|
||||
if (this.imageObserver) {
|
||||
this.imageObserver.disconnect();
|
||||
this.imageObserver = null;
|
||||
}
|
||||
|
||||
this.observedElements.clear();
|
||||
this.imagePreloadQueue.clear();
|
||||
|
||||
logger.debug("Intersection optimizer cleanup completed");
|
||||
}
|
||||
}
|
||||
|
||||
export const intersectionOptimizer = new IntersectionOptimizer();
|
|
@ -1,86 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { React } from "@webpack/common";
|
||||
|
||||
import { fastCache } from "./fastcache";
|
||||
import { statsTracker } from "./stats";
|
||||
|
||||
const logger = new Logger("Accelerator:MessageAccessory");
|
||||
|
||||
interface MessageAccessoryProps {
|
||||
message: any;
|
||||
}
|
||||
|
||||
let processedMessages = new Set<string>();
|
||||
|
||||
export function CacheIndicatorAccessory({ message }: MessageAccessoryProps) {
|
||||
const [showIndicator, setShowIndicator] = React.useState(false);
|
||||
const indicatorRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!message?.id || !message?.channel_id) return;
|
||||
|
||||
const messageKey = `${message.channel_id}:${message.id}`;
|
||||
|
||||
// Avoid processing the same message multiple times
|
||||
if (processedMessages.has(messageKey)) return;
|
||||
processedMessages.add(messageKey);
|
||||
|
||||
// Add slight delay to ensure cache has been checked
|
||||
setTimeout(() => {
|
||||
const cachedMessage = fastCache.getMessage(message.channel_id, message.id);
|
||||
|
||||
if (cachedMessage) {
|
||||
setShowIndicator(true);
|
||||
statsTracker.incrementMessagesServedFromCache(1);
|
||||
logger.debug(`Message ${message.id} served from cache`);
|
||||
}
|
||||
}, 100);
|
||||
}, [message?.id, message?.channel_id]);
|
||||
|
||||
// Ensure proper positioning after render
|
||||
React.useEffect(() => {
|
||||
if (showIndicator && indicatorRef.current) {
|
||||
const indicator = indicatorRef.current;
|
||||
const messageElement = indicator.closest('[class*="message"]') ||
|
||||
indicator.closest('[id^="chat-messages-"]') ||
|
||||
indicator.closest('[class*="messageListItem-"]');
|
||||
|
||||
if (messageElement) {
|
||||
const messageRect = messageElement.getBoundingClientRect();
|
||||
// Adjust position to be perfectly centered in the message highlight area
|
||||
indicator.style.top = "50%";
|
||||
indicator.style.right = "12px";
|
||||
}
|
||||
}
|
||||
}, [showIndicator]);
|
||||
|
||||
if (!showIndicator) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={indicatorRef}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
right: "12px",
|
||||
width: "10px",
|
||||
height: "10px",
|
||||
backgroundColor: "#3ba55c",
|
||||
borderRadius: "50%",
|
||||
zIndex: 100,
|
||||
transform: "translateY(-50%)",
|
||||
boxShadow: "0 0 0 2px rgba(32, 34, 37, 0.95), 0 2px 8px rgba(0, 0, 0, 0.5)",
|
||||
animation: "acceleratorCachePulse 2.5s ease-in-out infinite",
|
||||
cursor: "help",
|
||||
pointerEvents: "none" // Don't interfere with message interactions
|
||||
}}
|
||||
title="This message was served instantly from fastcache"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,214 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { FluxDispatcher, ChannelStore, GuildChannelStore, ChannelActionCreators, MessageActions } from "@webpack/common";
|
||||
import { Logger } from "@utils/Logger";
|
||||
|
||||
const logger = new Logger("Accelerator:Preloader");
|
||||
|
||||
interface PreloadedChannel {
|
||||
channelId: string;
|
||||
timestamp: number;
|
||||
messages: boolean;
|
||||
}
|
||||
|
||||
class ChannelPreloader {
|
||||
private preloadedChannels = new Map<string, PreloadedChannel>();
|
||||
private preloadQueue = new Set<string>();
|
||||
private maxPreloadAge = 5 * 60 * 1000; // 5 minutes
|
||||
private preloadDistance = 3;
|
||||
private isScrolling = false;
|
||||
private scrollTimeout: number | null = null;
|
||||
private lastScrollTime = 0;
|
||||
|
||||
init(distance: number) {
|
||||
this.preloadDistance = distance;
|
||||
this.setupScrollDetection();
|
||||
logger.info("Channel preloader initialized (scroll-aware)");
|
||||
}
|
||||
|
||||
private setupScrollDetection() {
|
||||
// Detect when user is actively scrolling to avoid interference
|
||||
const handleScroll = () => {
|
||||
this.isScrolling = true;
|
||||
this.lastScrollTime = Date.now();
|
||||
|
||||
if (this.scrollTimeout) {
|
||||
clearTimeout(this.scrollTimeout);
|
||||
}
|
||||
|
||||
// Consider scrolling finished after 150ms of no scroll events
|
||||
this.scrollTimeout = setTimeout(() => {
|
||||
this.isScrolling = false;
|
||||
}, 150);
|
||||
};
|
||||
|
||||
// Listen to scroll events on potential scroll containers
|
||||
document.addEventListener('scroll', handleScroll, { passive: true, capture: true });
|
||||
document.addEventListener('wheel', handleScroll, { passive: true, capture: true });
|
||||
}
|
||||
|
||||
async preloadAdjacent(guildId: string | null, currentChannelId: string, distance: number) {
|
||||
// Don't start new preloads while user is actively scrolling
|
||||
if (this.isScrolling || (Date.now() - this.lastScrollTime) < 500) {
|
||||
logger.debug("Skipping preload during scroll activity");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const adjacentChannels = this.getAdjacentChannels(guildId, currentChannelId, distance);
|
||||
|
||||
// More conservative preloading to avoid interfering with scroll
|
||||
const batchSize = 1; // Reduced from 2 to 1
|
||||
for (let i = 0; i < adjacentChannels.length; i += batchSize) {
|
||||
// Check if user started scrolling during preload
|
||||
if (this.isScrolling) {
|
||||
logger.debug("Stopping preload due to scroll activity");
|
||||
break;
|
||||
}
|
||||
|
||||
const batch = adjacentChannels.slice(i, i + batchSize);
|
||||
await Promise.all(batch.map(channelId => this.preloadChannel(channelId)));
|
||||
|
||||
// Longer delay between batches to be less aggressive
|
||||
if (i + batchSize < adjacentChannels.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup old preloaded channels
|
||||
this.cleanup();
|
||||
} catch (error) {
|
||||
logger.error("Failed to preload adjacent channels:", error);
|
||||
}
|
||||
}
|
||||
|
||||
private getAdjacentChannels(guildId: string | null, currentChannelId: string, distance: number): string[] {
|
||||
const channels: string[] = [];
|
||||
|
||||
try {
|
||||
if (guildId) {
|
||||
// Guild channels
|
||||
const guildChannels = GuildChannelStore.getChannels(guildId);
|
||||
const selectableChannels = guildChannels.SELECTABLE || [];
|
||||
|
||||
const currentIndex = selectableChannels.findIndex(ch => ch.channel.id === currentChannelId);
|
||||
if (currentIndex !== -1) {
|
||||
// Get channels before and after current
|
||||
for (let i = 1; i <= distance; i++) {
|
||||
const beforeIndex = currentIndex - i;
|
||||
const afterIndex = currentIndex + i;
|
||||
|
||||
if (beforeIndex >= 0) {
|
||||
channels.push(selectableChannels[beforeIndex].channel.id);
|
||||
}
|
||||
if (afterIndex < selectableChannels.length) {
|
||||
channels.push(selectableChannels[afterIndex].channel.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// DM channels - preload recent conversations
|
||||
const recentChannels = this.getRecentDMChannels(currentChannelId, distance);
|
||||
channels.push(...recentChannels);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to get adjacent channels:", error);
|
||||
}
|
||||
|
||||
return channels.filter(id => id !== currentChannelId);
|
||||
}
|
||||
|
||||
private getRecentDMChannels(excludeChannelId: string, count: number): string[] {
|
||||
// Get recent DM channels from Discord's internal stores
|
||||
try {
|
||||
const privateChannels = ChannelStore.getSortedPrivateChannels();
|
||||
return privateChannels
|
||||
.filter(channel => channel.id !== excludeChannelId)
|
||||
.slice(0, count)
|
||||
.map(channel => channel.id);
|
||||
} catch (error) {
|
||||
logger.error("Failed to get recent DM channels:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async preloadChannel(channelId: string) {
|
||||
if (this.preloadQueue.has(channelId)) return;
|
||||
if (this.isScrolling) return; // Don't start new preloads during scroll
|
||||
|
||||
const existingPreload = this.preloadedChannels.get(channelId);
|
||||
const now = Date.now();
|
||||
|
||||
// Skip if recently preloaded
|
||||
if (existingPreload && (now - existingPreload.timestamp) < this.maxPreloadAge) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.preloadQueue.add(channelId);
|
||||
|
||||
try {
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
if (!channel) return;
|
||||
|
||||
// Check again if user started scrolling
|
||||
if (this.isScrolling) {
|
||||
this.preloadQueue.delete(channelId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use Discord's internal preload system - but be more conservative
|
||||
if (channel.guild_id) {
|
||||
await ChannelActionCreators.preload(channel.guild_id, channelId);
|
||||
}
|
||||
|
||||
// Only preload messages for non-DM channels, and with smaller batch size
|
||||
if (channel.type !== 1 && channel.type !== 3 && !this.isScrolling) {
|
||||
await MessageActions.fetchMessages({
|
||||
channelId,
|
||||
limit: 25 // Reduced from 50 to 25
|
||||
});
|
||||
}
|
||||
|
||||
this.preloadedChannels.set(channelId, {
|
||||
channelId,
|
||||
timestamp: now,
|
||||
messages: true
|
||||
});
|
||||
|
||||
logger.debug(`Preloaded channel: ${channelId}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to preload channel ${channelId}:`, error);
|
||||
} finally {
|
||||
this.preloadQueue.delete(channelId);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
const now = Date.now();
|
||||
const toRemove: string[] = [];
|
||||
|
||||
for (const [channelId, preload] of this.preloadedChannels) {
|
||||
if ((now - preload.timestamp) > this.maxPreloadAge) {
|
||||
toRemove.push(channelId);
|
||||
}
|
||||
}
|
||||
|
||||
toRemove.forEach(channelId => this.preloadedChannels.delete(channelId));
|
||||
|
||||
if (toRemove.length > 0) {
|
||||
logger.debug(`Cleaned up ${toRemove.length} old preloaded channels`);
|
||||
}
|
||||
|
||||
// Cleanup scroll detection
|
||||
if (this.scrollTimeout) {
|
||||
clearTimeout(this.scrollTimeout);
|
||||
this.scrollTimeout = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const channelPreloader = new ChannelPreloader();
|
|
@ -1,513 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
|
||||
const logger = new Logger("Accelerator:Stats");
|
||||
|
||||
interface AcceleratorStats {
|
||||
channelSwitches: number;
|
||||
messagesLoaded: number;
|
||||
messagesCached: number;
|
||||
messagesServedFromCache: number;
|
||||
imagesOptimized: number;
|
||||
imagesCached: number;
|
||||
cacheHits: number;
|
||||
cacheMisses: number;
|
||||
averageLoadTime: number;
|
||||
startTime: number;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
interface ChannelSwitchTracking {
|
||||
channelId: string;
|
||||
startTime: number;
|
||||
}
|
||||
|
||||
class StatsTracker {
|
||||
private stats: AcceleratorStats;
|
||||
private loadTimes: number[] = [];
|
||||
private maxLoadTimeHistory = 50;
|
||||
private currentChannelSwitch: ChannelSwitchTracking | null = null;
|
||||
private imageLoadTimes = new Map<string, number>(); // Track when image requests start
|
||||
private processedImages = new Set<string>(); // Track which images we've seen before
|
||||
|
||||
constructor() {
|
||||
this.stats = this.getInitialStats();
|
||||
}
|
||||
|
||||
private getInitialStats(): AcceleratorStats {
|
||||
return {
|
||||
channelSwitches: 0,
|
||||
messagesLoaded: 0,
|
||||
messagesCached: 0,
|
||||
messagesServedFromCache: 0,
|
||||
imagesOptimized: 0,
|
||||
imagesCached: 0,
|
||||
cacheHits: 0,
|
||||
cacheMisses: 0,
|
||||
averageLoadTime: 0,
|
||||
startTime: Date.now(),
|
||||
lastUpdate: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
init() {
|
||||
this.stats = this.getInitialStats();
|
||||
this.setupImageCacheTracking();
|
||||
logger.info("Stats tracker initialized - tracking real Discord performance");
|
||||
}
|
||||
|
||||
// Track real Discord channel switching performance like messageFetchTimer
|
||||
trackChannelSwitchStart(channelId: string): void {
|
||||
this.currentChannelSwitch = {
|
||||
channelId,
|
||||
startTime: performance.now()
|
||||
};
|
||||
logger.debug(`Channel switch started: ${channelId}`);
|
||||
}
|
||||
|
||||
trackChannelSwitchEnd(channelId: string): void {
|
||||
if (!this.currentChannelSwitch || this.currentChannelSwitch.channelId !== channelId) {
|
||||
logger.debug(`Channel switch end without matching start: ${channelId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadTime = performance.now() - this.currentChannelSwitch.startTime;
|
||||
this.recordLoadTime(loadTime);
|
||||
this.incrementChannelSwitch();
|
||||
this.currentChannelSwitch = null;
|
||||
|
||||
logger.debug(`Channel switch completed: ${channelId} in ${loadTime.toFixed(1)}ms`);
|
||||
}
|
||||
|
||||
// Set up real image cache tracking by hooking into image loading
|
||||
private setupImageCacheTracking() {
|
||||
const statsTracker = this;
|
||||
|
||||
// Use MutationObserver to track all image elements added to DOM
|
||||
const imageObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach(mutation => {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as Element;
|
||||
|
||||
// Check if it's an image or contains images
|
||||
const images = element.tagName === 'IMG'
|
||||
? [element as HTMLImageElement]
|
||||
: Array.from(element.querySelectorAll('img'));
|
||||
|
||||
images.forEach(img => {
|
||||
this.trackImageElement(img);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Start observing
|
||||
imageObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// Track existing images
|
||||
document.querySelectorAll('img').forEach(img => {
|
||||
this.trackImageElement(img);
|
||||
});
|
||||
}
|
||||
|
||||
private trackImageElement(img: HTMLImageElement) {
|
||||
const src = img.src || img.getAttribute('data-src') || '';
|
||||
|
||||
// Only track Discord CDN images
|
||||
if (!src || (!src.includes('cdn.discordapp.com') && !src.includes('media.discordapp.net'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
this.imageLoadTimes.set(src, startTime);
|
||||
|
||||
// Check if we've seen this image before (cache scenario)
|
||||
const wasProcessed = this.processedImages.has(src);
|
||||
|
||||
const onLoad = () => {
|
||||
const endTime = performance.now();
|
||||
const loadTime = endTime - (this.imageLoadTimes.get(src) || endTime);
|
||||
|
||||
if (wasProcessed) {
|
||||
// Image was processed before, likely from cache
|
||||
this.incrementImagesCached();
|
||||
if (loadTime < 50) { // Very fast load likely means cache hit
|
||||
this.incrementCacheHit();
|
||||
}
|
||||
} else {
|
||||
// First time seeing this image
|
||||
this.incrementImagesOptimized();
|
||||
this.processedImages.add(src);
|
||||
if (loadTime >= 50) { // Slower load likely means cache miss
|
||||
this.incrementCacheMiss();
|
||||
}
|
||||
}
|
||||
|
||||
this.imageLoadTimes.delete(src);
|
||||
logger.debug(`Image loaded: ${src} in ${loadTime.toFixed(1)}ms (cached: ${wasProcessed})`);
|
||||
|
||||
// Clean up listeners
|
||||
img.removeEventListener('load', onLoad);
|
||||
img.removeEventListener('error', onError);
|
||||
};
|
||||
|
||||
const onError = () => {
|
||||
this.imageLoadTimes.delete(src);
|
||||
img.removeEventListener('load', onLoad);
|
||||
img.removeEventListener('error', onError);
|
||||
};
|
||||
|
||||
// If image is already loaded, track it immediately
|
||||
if (img.complete && img.naturalWidth > 0) {
|
||||
setTimeout(onLoad, 0);
|
||||
} else {
|
||||
img.addEventListener('load', onLoad);
|
||||
img.addEventListener('error', onError);
|
||||
}
|
||||
}
|
||||
|
||||
incrementChannelSwitch() {
|
||||
this.stats.channelSwitches++;
|
||||
this.updateTimestamp();
|
||||
logger.debug(`Channel switch tracked: ${this.stats.channelSwitches}`);
|
||||
}
|
||||
|
||||
incrementMessagesLoaded(count: number) {
|
||||
this.stats.messagesLoaded += count;
|
||||
this.updateTimestamp();
|
||||
logger.debug(`Messages loaded: +${count} (total: ${this.stats.messagesLoaded})`);
|
||||
}
|
||||
|
||||
incrementMessagesCached(count: number) {
|
||||
this.stats.messagesCached += count;
|
||||
this.updateTimestamp();
|
||||
logger.debug(`Messages cached: +${count} (total: ${this.stats.messagesCached})`);
|
||||
}
|
||||
|
||||
incrementMessagesServedFromCache(count: number) {
|
||||
this.stats.messagesServedFromCache += count;
|
||||
this.updateTimestamp();
|
||||
logger.debug(`Messages served from cache: +${count} (total: ${this.stats.messagesServedFromCache})`);
|
||||
}
|
||||
|
||||
incrementImagesOptimized() {
|
||||
this.stats.imagesOptimized++;
|
||||
this.updateTimestamp();
|
||||
logger.debug(`Images optimized: ${this.stats.imagesOptimized}`);
|
||||
}
|
||||
|
||||
incrementImagesCached() {
|
||||
this.stats.imagesCached++;
|
||||
this.updateTimestamp();
|
||||
logger.debug(`Images cached: ${this.stats.imagesCached}`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
incrementCacheHit() {
|
||||
this.stats.cacheHits++;
|
||||
this.updateTimestamp();
|
||||
logger.debug(`Cache hit: ${this.stats.cacheHits}`);
|
||||
}
|
||||
|
||||
incrementCacheMiss() {
|
||||
this.stats.cacheMisses++;
|
||||
this.updateTimestamp();
|
||||
logger.debug(`Cache miss: ${this.stats.cacheMisses}`);
|
||||
}
|
||||
|
||||
updateCacheStats(cacheStats: any): void {
|
||||
if (cacheStats.hits !== undefined) this.stats.cacheHits = cacheStats.hits;
|
||||
if (cacheStats.misses !== undefined) this.stats.cacheMisses = cacheStats.misses;
|
||||
if (cacheStats.totalMessagesCached !== undefined) this.stats.messagesCached = cacheStats.totalMessagesCached;
|
||||
this.updateTimestamp();
|
||||
logger.debug(`Cache stats updated:`, cacheStats);
|
||||
}
|
||||
|
||||
recordLoadTime(time: number) {
|
||||
// Only record realistic load times (filter out obviously wrong values)
|
||||
if (time > 0 && time < 30000) { // Between 0 and 30 seconds
|
||||
this.loadTimes.push(time);
|
||||
if (this.loadTimes.length > this.maxLoadTimeHistory) {
|
||||
this.loadTimes.shift();
|
||||
}
|
||||
|
||||
this.stats.averageLoadTime = this.loadTimes.reduce((a, b) => a + b, 0) / this.loadTimes.length;
|
||||
this.updateTimestamp();
|
||||
logger.debug(`Load time recorded: ${time.toFixed(1)}ms (avg: ${this.stats.averageLoadTime.toFixed(1)}ms)`);
|
||||
}
|
||||
}
|
||||
|
||||
private updateTimestamp() {
|
||||
this.stats.lastUpdate = Date.now();
|
||||
}
|
||||
|
||||
getStats(): AcceleratorStats {
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
getFormattedUptime(): string {
|
||||
const uptime = Date.now() - this.stats.startTime;
|
||||
const minutes = Math.floor(uptime / 60000);
|
||||
const seconds = Math.floor((uptime % 60000) / 1000);
|
||||
return `${minutes}m ${seconds}s`;
|
||||
}
|
||||
|
||||
getCacheHitRate(): number {
|
||||
const total = this.stats.cacheHits + this.stats.cacheMisses;
|
||||
return total > 0 ? (this.stats.cacheHits / total) * 100 : 0;
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
// Reset tracking state
|
||||
this.currentChannelSwitch = null;
|
||||
this.imageLoadTimes.clear();
|
||||
this.processedImages.clear();
|
||||
this.stats = this.getInitialStats();
|
||||
this.loadTimes = [];
|
||||
logger.debug("Stats tracker cleanup completed");
|
||||
}
|
||||
}
|
||||
|
||||
export const statsTracker = new StatsTracker();
|
||||
|
||||
// Floating Stats Window using vanilla DOM
|
||||
export class FloatingStats {
|
||||
private static container: HTMLDivElement | null = null;
|
||||
private static isVisible = false;
|
||||
private static updateInterval: number | null = null;
|
||||
private static isDragging = false;
|
||||
private static dragOffset = { x: 0, y: 0 };
|
||||
private static position = { x: window.innerWidth - 320, y: 20 };
|
||||
private static isMinimized = false;
|
||||
|
||||
static show() {
|
||||
if (this.isVisible) return;
|
||||
|
||||
this.createContainer();
|
||||
this.setupEventListeners();
|
||||
this.startUpdating();
|
||||
this.isVisible = true;
|
||||
|
||||
logger.info("Floating stats window shown");
|
||||
}
|
||||
|
||||
static hide() {
|
||||
if (!this.isVisible) return;
|
||||
|
||||
if (this.container) {
|
||||
document.body.removeChild(this.container);
|
||||
this.container = null;
|
||||
}
|
||||
|
||||
if (this.updateInterval) {
|
||||
clearInterval(this.updateInterval);
|
||||
this.updateInterval = null;
|
||||
}
|
||||
|
||||
this.isVisible = false;
|
||||
logger.info("Floating stats window hidden");
|
||||
}
|
||||
|
||||
static toggle() {
|
||||
if (this.isVisible) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
|
||||
private static createContainer() {
|
||||
this.container = document.createElement("div");
|
||||
this.container.id = "accelerator-stats-container";
|
||||
|
||||
this.updateStyles();
|
||||
this.updateContent();
|
||||
|
||||
document.body.appendChild(this.container);
|
||||
}
|
||||
|
||||
private static updateStyles() {
|
||||
if (!this.container) return;
|
||||
|
||||
Object.assign(this.container.style, {
|
||||
position: "fixed",
|
||||
top: `${this.position.y}px`,
|
||||
left: `${this.position.x}px`,
|
||||
width: "300px",
|
||||
minHeight: this.isMinimized ? "40px" : "auto",
|
||||
maxHeight: this.isMinimized ? "40px" : "500px",
|
||||
backgroundColor: "var(--background-secondary)",
|
||||
border: "1px solid var(--background-secondary-alt)",
|
||||
borderRadius: "8px",
|
||||
padding: "12px",
|
||||
fontSize: "12px",
|
||||
fontFamily: "var(--font-primary)",
|
||||
color: "var(--text-normal)",
|
||||
zIndex: "9999",
|
||||
userSelect: "none",
|
||||
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.15)",
|
||||
backdropFilter: "blur(10px)",
|
||||
overflow: "hidden",
|
||||
transition: "all 0.2s ease"
|
||||
});
|
||||
}
|
||||
|
||||
private static updateContent() {
|
||||
if (!this.container) return;
|
||||
|
||||
const stats = statsTracker.getStats();
|
||||
const uptime = statsTracker.getFormattedUptime();
|
||||
const cacheRate = statsTracker.getCacheHitRate();
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000) return (num / 1000).toFixed(1) + "k";
|
||||
return num.toString();
|
||||
};
|
||||
|
||||
const formatLoadTime = (time: number): string => {
|
||||
return time < 1000 ? `${Math.round(time)}ms` : `${(time / 1000).toFixed(1)}s`;
|
||||
};
|
||||
|
||||
const formatCacheRate = (rate: number): string => {
|
||||
return rate.toFixed(1) + "%";
|
||||
};
|
||||
|
||||
const headerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: ${this.isMinimized ? '0' : '8px'}; padding-bottom: 4px; border-bottom: 1px solid var(--background-modifier-accent);">
|
||||
<span style="font-weight: 600; color: var(--text-brand);">🚀 Accelerator Stats</span>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button id="accelerator-minimize" style="background: none; border: none; color: var(--text-muted); cursor: pointer; padding: 2px; font-size: 14px;">${this.isMinimized ? '📈' : '📉'}</button>
|
||||
<button id="accelerator-close" style="background: none; border: none; color: var(--text-muted); cursor: pointer; padding: 2px; font-size: 12px;">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const contentHTML = this.isMinimized ? '' : `
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 11px;">
|
||||
<div style="background: var(--background-tertiary); padding: 6px; border-radius: 4px;">
|
||||
<div style="color: var(--text-muted); margin-bottom: 2px;">Channel Switches</div>
|
||||
<div style="font-weight: 600; color: var(--text-brand);">${formatNumber(stats.channelSwitches)}</div>
|
||||
</div>
|
||||
<div style="background: var(--background-tertiary); padding: 6px; border-radius: 4px;">
|
||||
<div style="color: var(--text-muted); margin-bottom: 2px;">Channel Load Time</div>
|
||||
<div style="font-weight: 600; color: ${stats.averageLoadTime < 1000 ? 'var(--text-positive)' : stats.averageLoadTime < 2000 ? 'var(--text-warning)' : 'var(--text-danger)'};">${formatLoadTime(stats.averageLoadTime)}</div>
|
||||
</div>
|
||||
<div style="background: var(--background-tertiary); padding: 6px; border-radius: 4px;">
|
||||
<div style="color: var(--text-muted); margin-bottom: 2px;">Messages from Discord</div>
|
||||
<div style="font-weight: 600; color: var(--text-normal);">${formatNumber(stats.messagesLoaded)}</div>
|
||||
</div>
|
||||
<div style="background: var(--background-tertiary); padding: 6px; border-radius: 4px;">
|
||||
<div style="color: var(--text-muted); margin-bottom: 2px;">Messages from Cache</div>
|
||||
<div style="font-weight: 600; color: var(--text-positive);">${formatNumber(stats.messagesServedFromCache)}</div>
|
||||
</div>
|
||||
<div style="background: var(--background-tertiary); padding: 6px; border-radius: 4px;">
|
||||
<div style="color: var(--text-muted); margin-bottom: 2px;">Messages Stored</div>
|
||||
<div style="font-weight: 600; color: var(--text-brand);">${formatNumber(stats.messagesCached)}</div>
|
||||
</div>
|
||||
<div style="background: var(--background-tertiary); padding: 6px; border-radius: 4px;">
|
||||
<div style="color: var(--text-muted); margin-bottom: 2px;">Message Cache Rate</div>
|
||||
<div style="font-weight: 600; color: ${cacheRate > 70 ? 'var(--text-positive)' : cacheRate > 40 ? 'var(--text-warning)' : 'var(--text-danger)'};">${formatCacheRate(cacheRate)}</div>
|
||||
</div>
|
||||
<div style="background: var(--background-tertiary); padding: 6px; border-radius: 4px;">
|
||||
<div style="color: var(--text-muted); margin-bottom: 2px;">Images Preloaded</div>
|
||||
<div style="font-weight: 600; color: var(--text-positive);">${formatNumber(stats.imagesOptimized)}</div>
|
||||
</div>
|
||||
<div style="background: var(--background-tertiary); padding: 6px; border-radius: 4px;">
|
||||
<div style="color: var(--text-muted); margin-bottom: 2px;">Images from Cache</div>
|
||||
<div style="font-weight: 600; color: var(--text-positive);">${formatNumber(stats.imagesCached)}</div>
|
||||
</div>
|
||||
<div style="background: var(--background-tertiary); padding: 6px; border-radius: 4px;">
|
||||
<div style="color: var(--text-muted); margin-bottom: 2px;">Uptime</div>
|
||||
<div style="font-weight: 600; color: var(--text-muted);">${uptime}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.container.innerHTML = headerHTML + contentHTML;
|
||||
this.setupToggleButton();
|
||||
}
|
||||
|
||||
private static setupToggleButton() {
|
||||
const minimizeBtn = document.getElementById("accelerator-minimize");
|
||||
const closeBtn = document.getElementById("accelerator-close");
|
||||
|
||||
if (minimizeBtn) {
|
||||
minimizeBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
this.isMinimized = !this.isMinimized;
|
||||
this.updateStyles();
|
||||
this.updateContent();
|
||||
};
|
||||
}
|
||||
|
||||
if (closeBtn) {
|
||||
closeBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
this.hide();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static setupEventListeners() {
|
||||
if (!this.container) return;
|
||||
|
||||
// Dragging functionality
|
||||
this.container.onmousedown = (e) => {
|
||||
if ((e.target as HTMLElement).tagName === "BUTTON") return;
|
||||
|
||||
this.isDragging = true;
|
||||
this.dragOffset.x = e.clientX - this.position.x;
|
||||
this.dragOffset.y = e.clientY - this.position.y;
|
||||
|
||||
if (this.container) {
|
||||
this.container.style.cursor = "grabbing";
|
||||
this.container.style.opacity = "0.8";
|
||||
}
|
||||
};
|
||||
|
||||
document.onmousemove = (e) => {
|
||||
if (!this.isDragging || !this.container) return;
|
||||
|
||||
this.position.x = e.clientX - this.dragOffset.x;
|
||||
this.position.y = e.clientY - this.dragOffset.y;
|
||||
|
||||
// Keep within screen bounds
|
||||
this.position.x = Math.max(0, Math.min(window.innerWidth - 300, this.position.x));
|
||||
this.position.y = Math.max(0, Math.min(window.innerHeight - 100, this.position.y));
|
||||
|
||||
this.container.style.left = `${this.position.x}px`;
|
||||
this.container.style.top = `${this.position.y}px`;
|
||||
};
|
||||
|
||||
document.onmouseup = () => {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
this.isDragging = false;
|
||||
if (this.container) {
|
||||
this.container.style.cursor = "default";
|
||||
this.container.style.opacity = "1";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static startUpdating() {
|
||||
if (this.updateInterval) clearInterval(this.updateInterval);
|
||||
|
||||
this.updateInterval = setInterval(() => {
|
||||
if (this.isVisible && this.container) {
|
||||
this.updateContent();
|
||||
}
|
||||
}, 1000) as any;
|
||||
}
|
||||
}
|
|
@ -1,171 +0,0 @@
|
|||
/*
|
||||
* Accelerator Stats Window - The ONLY CSS component
|
||||
* This contains ONLY styles for the floating stats window
|
||||
* NO performance CSS that could interfere with Discord's layout
|
||||
*/
|
||||
|
||||
.accelerator-stats-window {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
background: rgba(32, 34, 37, 0.95);
|
||||
border: 1px solid #40444b;
|
||||
border-radius: 8px;
|
||||
backdrop-filter: blur(10px);
|
||||
font-family: 'gg sans', 'Noto Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
color: #dcddde;
|
||||
min-width: 280px;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
user-select: none;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.accelerator-stats-header {
|
||||
padding: 12px 16px 8px;
|
||||
border-bottom: 1px solid #40444b;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: linear-gradient(90deg, #5865f2, #7289da);
|
||||
color: white;
|
||||
border-radius: 7px 7px 0 0;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.accelerator-stats-content {
|
||||
padding: 12px 16px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.accelerator-stats-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.accelerator-stats-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.accelerator-stats-content::-webkit-scrollbar-thumb {
|
||||
background: #40444b;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.accelerator-stats-section {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.accelerator-stats-section h4 {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #b9bbbe;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.accelerator-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.accelerator-stats-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.accelerator-stats-label {
|
||||
color: #b9bbbe;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.accelerator-stats-value {
|
||||
color: #dcddde;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.accelerator-stats-value.positive {
|
||||
color: #3ba55c;
|
||||
}
|
||||
|
||||
.accelerator-stats-value.neutral {
|
||||
color: #faa61a;
|
||||
}
|
||||
|
||||
.accelerator-stats-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #dcddde;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.accelerator-stats-toggle:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.accelerator-stats-window.minimized .accelerator-stats-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.accelerator-stats-window.minimized {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
/* Cache Indicator - Shows when a message was served from fastcache */
|
||||
.accelerator-cache-indicator {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #3ba55c;
|
||||
border-radius: 50%;
|
||||
z-index: 10;
|
||||
box-shadow: 0 0 0 1px rgba(32, 34, 37, 0.9), 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
animation: accelerator-cache-pulse 2s ease-out;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
@keyframes acceleratorCachePulse {
|
||||
0% {
|
||||
transform: translateY(-50%) scale(1);
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 0 2px rgba(32, 34, 37, 0.95), 0 2px 8px rgba(0, 0, 0, 0.5), 0 0 0 0 rgba(59, 165, 92, 1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-50%) scale(1.4);
|
||||
opacity: 0.6;
|
||||
box-shadow: 0 0 0 2px rgba(32, 34, 37, 0.95), 0 2px 8px rgba(0, 0, 0, 0.5), 0 0 0 12px rgba(59, 165, 92, 0.6);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(-50%) scale(1);
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 0 2px rgba(32, 34, 37, 0.95), 0 2px 8px rgba(0, 0, 0, 0.5), 0 0 0 0 rgba(59, 165, 92, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure the message container has relative positioning for the indicator */
|
||||
[class*="message-"],
|
||||
[id^="chat-messages-"],
|
||||
[data-list-item-id^="chat-messages"] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Additional support for Discord's message structure */
|
||||
[class*="messageListItem-"],
|
||||
[class*="groupStart-"],
|
||||
[class*="wrapper-"] {
|
||||
position: relative;
|
||||
}
|
|
@ -613,10 +613,6 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
} satisfies Record<string, Dev>);
|
||||
|
||||
export const EquicordDevs = Object.freeze({
|
||||
galpt: {
|
||||
name: "galpt",
|
||||
id: 631418827841863712n
|
||||
},
|
||||
nobody: {
|
||||
name: "nobody",
|
||||
id: 0n
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue