Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions src/components/CodeSyncProvider/CodeSyncProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { migrateSelection as migrateSelectionRaw } from "@fluffylabs/links-metad
import type { ISelectionParams, ISynctexBlock, ISynctexBlockId, ISynctexData } from "@fluffylabs/links-metadata";
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import type { PropsWithChildren } from "react";
import { isFeatureEnabled } from "../../devtools";
import { type ILocationContext, LocationContext } from "../LocationProvider/LocationProvider";
import { debugDrawBlock } from "./debugDrawBlock";
import { useSynctexStore } from "./hooks/useSynctexStore";
import { useTexStore } from "./hooks/useTexStore";

Expand All @@ -28,6 +30,13 @@ const BIBLIOGRAPHY_TITLE = "References";

export const CodeSyncContext = createContext<ICodeSyncContext | null>(null);

const isPathologicalBlock = (block: { width: number; height: number }) => {
// Block is pathological if:
// - Height exceeds 15% of page height, OR
// - Width exceeds 95% of page width
return block.height > 0.15 || block.width > 0.95;
};

export function CodeSyncProvider({ children }: PropsWithChildren) {
const [synctexData, setSynctexData] = useState<ISynctexData>();
const texStore = useTexStore();
Expand All @@ -37,7 +46,8 @@ export function CodeSyncProvider({ children }: PropsWithChildren) {

useEffect(() => {
async function loadSynctex() {
setSynctexData(await synctexStore.getSynctex(version));
const synctex = await synctexStore.getSynctex(version);
setSynctexData(synctex);
}

if (version) {
Expand All @@ -55,6 +65,11 @@ export function CodeSyncProvider({ children }: PropsWithChildren) {

for (let i = 0; i < blocksInCurrPage.length; i++) {
const currBlock = blocksInCurrPage[i];

if (isPathologicalBlock(currBlock)) {
continue;
}

if (
left >= currBlock.left - BLOCK_MATCHING_TOLERANCE_AS_FRACTION_OF_PAGE_WIDTH &&
left <= currBlock.left + currBlock.width + BLOCK_MATCHING_TOLERANCE_AS_FRACTION_OF_PAGE_WIDTH &&
Expand Down Expand Up @@ -97,9 +112,18 @@ export function CodeSyncProvider({ children }: PropsWithChildren) {
}

// todo: for now we assume selections are within one page
return (
synctexData.blocksByPage.get(startBlockId.pageNumber)?.slice(startBlockId.index, endBlockId.index + 1) || []
);
const rangeOfBlocks =
synctexData.blocksByPage.get(startBlockId.pageNumber)?.slice(startBlockId.index, endBlockId.index + 1) ?? [];

const goodBlocks = rangeOfBlocks.filter((block) => !isPathologicalBlock(block)) || [];

if (isFeatureEnabled("DEBUG_DRAW_BLOCKS")) {
for (const block of goodBlocks) {
debugDrawBlock(startBlockId.pageNumber, block);
}
}

return goodBlocks;
},
[synctexData],
);
Expand Down
58 changes: 58 additions & 0 deletions src/components/CodeSyncProvider/debugDrawBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const statePerPage = new Map<number, ImageData>();

function drawRectangle(page: number, rectParams: { width: number; height: number; top: number; left: number }) {
const canvas = document.querySelector(`[data-page-number="${page}"] canvas`) as HTMLCanvasElement;

if (!canvas) return;

const context = canvas.getContext("2d");

if (!context) return;

if (!statePerPage.has(page)) {
const savedState = context.getImageData(0, 0, canvas.width, canvas.height);
statePerPage.set(page, savedState);
}

// Extract parameters from the input object
const { height, width, top, left } = rectParams;

// Convert normalized coordinates to pixel values
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;

const x1 = left * canvasWidth;
const y1 = top * canvasHeight;
const rectWidth = width * canvasWidth;
const rectHeight = height * canvasHeight;

// Draw the rectangle outline
context.beginPath();
context.strokeStyle = "red";
context.lineWidth = 6;

context.moveTo(x1, y1);
context.lineTo(x1 + rectWidth, y1);
context.lineTo(x1 + rectWidth, y1 + rectHeight); // Fixed: should be + not -
context.lineTo(x1, y1 + rectHeight);
context.closePath();
context.stroke();

// Return an undo function
return function undo() {
const savedState = statePerPage.get(page);
if (savedState) {
context.putImageData(savedState, 0, 0);
}
};
}

export function debugDrawBlock(page: number, rectParams: { width: number; height: number; top: number; left: number }) {
const removeCallback = drawRectangle(page, rectParams);

if (!removeCallback) return;

setTimeout(() => {
removeCallback();
}, 2500);
}
4 changes: 4 additions & 0 deletions src/components/PdfViewer/PdfViewer.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@
.pdfViewer .textLayer ::selection {
background: rgba(0, 50, 255, 0.25);
}

.pdfViewer .textLayer .endOfContent, .pdfViewer .textLayer br{
pointer-events: none;
}
162 changes: 162 additions & 0 deletions src/devtools/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
export interface FeatureFlag {
key: string;
name: string;
description: string;
defaultValue: boolean;
}

export const FEATURE_FLAGS = {
DEBUG_DRAW_BLOCKS: {
key: "debug_draw_blocks",
name: "Debug Draw Blocks",
description: "Draws visual rectangles around synctex blocks for debugging",
defaultValue: false,
},
} as const;

export type FeatureFlagKey = keyof typeof FEATURE_FLAGS;

class FeatureFlagsManager {
private static STORAGE_PREFIX = "gp_feature_flag_";

private isDevelopment(): boolean {
return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
}

private getStorageKey(flagKey: string): string {
return `${FeatureFlagsManager.STORAGE_PREFIX}${flagKey}`;
}

isEnabled(flagKey: FeatureFlagKey): boolean {
if (!this.isDevelopment()) {
return false;
}

const flag = FEATURE_FLAGS[flagKey];
const storedValue = sessionStorage.getItem(this.getStorageKey(flag.key));

if (storedValue !== null) {
return storedValue === "true";
}

return flag.defaultValue;
}

toggle(flagKey: FeatureFlagKey): boolean {
if (!this.isDevelopment()) {
console.warn("Feature flags are only available in development environment");
return false;
}

const flag = FEATURE_FLAGS[flagKey];
const currentValue = this.isEnabled(flagKey);
const newValue = !currentValue;

sessionStorage.setItem(this.getStorageKey(flag.key), String(newValue));

console.log(`🚩 Feature flag "${flag.name}" is now ${newValue ? "ENABLED ✅" : "DISABLED ❌"}`);

return newValue;
}

enable(flagKey: FeatureFlagKey): void {
if (!this.isDevelopment()) {
console.warn("Feature flags are only available in development environment");
return;
}

const flag = FEATURE_FLAGS[flagKey];
sessionStorage.setItem(this.getStorageKey(flag.key), "true");
console.log(`🚩 Feature flag "${flag.name}" is now ENABLED ✅`);
}

disable(flagKey: FeatureFlagKey): void {
if (!this.isDevelopment()) {
console.warn("Feature flags are only available in development environment");
return;
}

const flag = FEATURE_FLAGS[flagKey];
sessionStorage.setItem(this.getStorageKey(flag.key), "false");
console.log(`🚩 Feature flag "${flag.name}" is now DISABLED ❌`);
}

reset(flagKey: FeatureFlagKey): void {
if (!this.isDevelopment()) {
console.warn("Feature flags are only available in development environment");
return;
}

const flag = FEATURE_FLAGS[flagKey];
sessionStorage.removeItem(this.getStorageKey(flag.key));
console.log(`🚩 Feature flag "${flag.name}" reset to default: ${flag.defaultValue ? "ENABLED ✅" : "DISABLED ❌"}`);
}

resetAll(): void {
if (!this.isDevelopment()) {
console.warn("Feature flags are only available in development environment");
return;
}

for (const [, flag] of Object.entries(FEATURE_FLAGS)) {
sessionStorage.removeItem(this.getStorageKey(flag.key));
}

console.log("🚩 All feature flags have been reset to defaults");
this.listFlags();
}

listFlags(): void {
if (!this.isDevelopment()) {
console.warn("Feature flags are only available in development environment");
return;
}

console.group("🚩 Available Feature Flags");

for (const [key, flag] of Object.entries(FEATURE_FLAGS)) {
const isEnabled = this.isEnabled(key as FeatureFlagKey);
const status = isEnabled ? "✅ ENABLED" : "❌ DISABLED";

console.log(
`%c${status}%c ${flag.name} (${key})\n ${flag.description}`,
`color: ${isEnabled ? "green" : "gray"}; font-weight: bold`,
"color: inherit; font-weight: normal",
);
}

console.groupEnd();

console.log("\n💡 Use window.gpDev to interact with feature flags");
}

help(): void {
if (!this.isDevelopment()) {
console.warn("Feature flags are only available in development environment");
return;
}

console.group("📚 Feature Flags Help");
console.log("%cAvailable commands:", "font-weight: bold; font-size: 14px");
console.log(" gpDev.listFlags() - List all available feature flags");
console.log(' gpDev.toggle("FLAG_KEY") - Toggle a specific flag on/off');
console.log(' gpDev.enable("FLAG_KEY") - Enable a specific flag');
console.log(' gpDev.disable("FLAG_KEY") - Disable a specific flag');
console.log(' gpDev.reset("FLAG_KEY") - Reset flag to default value');
console.log(" gpDev.resetAll() - Reset all flags to defaults");
console.log(" gpDev.help() - Show this help message");
console.log("\n%cAvailable flag keys:", "font-weight: bold; font-size: 14px");

for (const [key, flag] of Object.entries(FEATURE_FLAGS)) {
console.log(` "${key}" - ${flag.name}`);
}

console.groupEnd();
}
}

export const featureFlags = new FeatureFlagsManager();

export function isFeatureEnabled(flagKey: FeatureFlagKey): boolean {
return featureFlags.isEnabled(flagKey);
}
10 changes: 10 additions & 0 deletions src/devtools/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export { featureFlags, isFeatureEnabled, FEATURE_FLAGS } from "./featureFlags";
export type { FeatureFlag, FeatureFlagKey } from "./featureFlags";

export { useFeatureFlag, withFeatureFlag } from "./useFeatureFlag";

export { initDevTools } from "./initDevTools";

export function isDevelopment(): boolean {
return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
}
46 changes: 46 additions & 0 deletions src/devtools/initDevTools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { type FeatureFlagKey, featureFlags } from "./featureFlags";

declare global {
interface Window {
gpDev: {
listFlags: () => void;
toggle: (flagKey: FeatureFlagKey) => boolean;
enable: (flagKey: FeatureFlagKey) => void;
disable: (flagKey: FeatureFlagKey) => void;
reset: (flagKey: FeatureFlagKey) => void;
resetAll: () => void;
help: () => void;
};
}
}

export function initDevTools(): void {
if (window.location.hostname !== "localhost" && window.location.hostname !== "127.0.0.1") {
return;
}

window.gpDev = {
listFlags: () => featureFlags.listFlags(),
toggle: (flagKey: FeatureFlagKey) => featureFlags.toggle(flagKey),
enable: (flagKey: FeatureFlagKey) => featureFlags.enable(flagKey),
disable: (flagKey: FeatureFlagKey) => featureFlags.disable(flagKey),
reset: (flagKey: FeatureFlagKey) => featureFlags.reset(flagKey),
resetAll: () => featureFlags.resetAll(),
help: () => featureFlags.help(),
};

console.log(
"%c🛠️ Graypaper Reader Dev Tools Loaded",
"background: #4a5568; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold; font-size: 14px",
);
console.log(
"💡 Type %cgpDev.help()%c to see available commands",
"font-family: monospace; background: #edf2f7; padding: 2px 4px; border-radius: 2px",
"",
);
console.log(
"🚩 Type %cgpDev.listFlags()%c to see all feature flags",
"font-family: monospace; background: #edf2f7; padding: 2px 4px; border-radius: 2px",
"",
);
}
Loading