mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
# Description of Changes Vite currently warns that when it's bundling our code that the chunk size is way too high because most of the imports are static so it can't split them into smaller chunks. This PR changes a few key areas to use lazy imports to try and make the chunks as small as possible with minimal code changes. Vite's warnings kick in at minified chunks being >500kB, and we've got a little way to go still to reach that, but we can keep chipping away at this and I'd rather get the biggest wins done now. I've also included Lighthouse scores because there's been discussion about improving ours recently. It's not the aim of this PR to improve it, but it's nice that it makes it a little better. ## Current main chunks Build split into 12 chunks. Largest chunk in build is: ``` [frontend:build] dist/assets/index-B6JiWDxZ.js 5,175.51 kB │ gzip: 1,495.85 kB ``` <img width="1442" height="775" alt="image" src="https://github.com/user-attachments/assets/b0e8a3fa-4ef3-4ccd-8c1d-bfed2d99bd27" /> Lighthouse score: <img width="423" height="146" alt="before" src="https://github.com/user-attachments/assets/c62056e8-2e77-49a6-a1ae-f08ec8021fb3" /> ## This PR's chunks Build split into 176 chunks. Largest chunk in build is: ``` [frontend:build] dist/assets/index-qCgeCY4B.js 2,878.54 kB │ gzip: 861.03 kB ``` <img width="1447" height="776" alt="image" src="https://github.com/user-attachments/assets/8d0c3cf0-cc25-41c3-b114-4940d3e99349" /> Lighthouse score: <img width="402" height="145" alt="after" src="https://github.com/user-attachments/assets/99a26eb3-bd15-4b92-bf22-82b58b458f52" /> --------- Co-authored-by: EthanHealy01 <[email protected]>
379 lines
13 KiB
TypeScript
379 lines
13 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
|
|
import { Group } from "@mantine/core";
|
|
import { useSidebarContext } from "@app/contexts/SidebarContext";
|
|
import { useDocumentMeta } from "@app/hooks/useDocumentMeta";
|
|
import { useBaseUrl } from "@app/hooks/useBaseUrl";
|
|
import { useIsMobile } from "@app/hooks/useIsMobile";
|
|
import { useAppConfig } from "@app/contexts/AppConfigContext";
|
|
import { LogoIcon } from "@app/components/shared/LogoIcon";
|
|
import { Wordmark } from "@app/components/shared/Wordmark";
|
|
import { useFileContext } from "@app/contexts/file/fileHooks";
|
|
import {
|
|
useNavigationState,
|
|
useNavigationActions,
|
|
} from "@app/contexts/NavigationContext";
|
|
import { useViewer } from "@app/contexts/ViewerContext";
|
|
import AppsIcon from "@mui/icons-material/AppsRounded";
|
|
|
|
import ToolPanel from "@app/components/tools/ToolPanel";
|
|
import Workbench from "@app/components/layout/Workbench";
|
|
import QuickAccessBar from "@app/components/shared/QuickAccessBar";
|
|
import RightRail from "@app/components/shared/RightRail";
|
|
import FileManager from "@app/components/FileManager";
|
|
import LocalIcon from "@app/components/shared/LocalIcon";
|
|
import { useFilesModalContext } from "@app/contexts/FilesModalContext";
|
|
import AppConfigModal from "@app/components/shared/AppConfigModalLazy";
|
|
import { getStartupNavigationAction } from "@app/utils/homePageNavigation";
|
|
import { HomePageExtensions } from "@app/components/home/HomePageExtensions";
|
|
|
|
import "@app/pages/HomePage.css";
|
|
|
|
type MobileView = "tools" | "workbench";
|
|
|
|
export default function HomePage() {
|
|
const { t } = useTranslation();
|
|
const { sidebarRefs } = useSidebarContext();
|
|
|
|
const { quickAccessRef } = sidebarRefs;
|
|
|
|
const {
|
|
selectedTool,
|
|
selectedToolKey,
|
|
handleToolSelect,
|
|
handleBackToTools,
|
|
readerMode,
|
|
setLeftPanelView,
|
|
toolAvailability,
|
|
customWorkbenchViews,
|
|
} = useToolWorkflow();
|
|
|
|
const { openFilesModal } = useFilesModalContext();
|
|
const { config } = useAppConfig();
|
|
const isMobile = useIsMobile();
|
|
const sliderRef = useRef<HTMLDivElement | null>(null);
|
|
const [activeMobileView, setActiveMobileView] = useState<MobileView>("tools");
|
|
const isProgrammaticScroll = useRef(false);
|
|
const [configModalOpen, setConfigModalOpen] = useState(false);
|
|
|
|
const { activeFiles } = useFileContext();
|
|
const navigationState = useNavigationState();
|
|
const { actions } = useNavigationActions();
|
|
const { setActiveFileIndex } = useViewer();
|
|
const prevFileCountRef = useRef(activeFiles.length);
|
|
|
|
// Startup/open transition behavior:
|
|
// - opening exactly 1 file from empty -> viewer (unless already in fileEditor)
|
|
// - opening 2+ files from empty -> fileEditor
|
|
useEffect(() => {
|
|
const prevCount = prevFileCountRef.current;
|
|
const currentCount = activeFiles.length;
|
|
|
|
console.log("[HomePage] Navigation effect triggered:", {
|
|
prevCount,
|
|
currentCount,
|
|
currentWorkbench: navigationState.workbench,
|
|
selectedToolKey,
|
|
});
|
|
|
|
const action = getStartupNavigationAction(
|
|
prevCount,
|
|
currentCount,
|
|
selectedToolKey,
|
|
navigationState.workbench,
|
|
);
|
|
|
|
console.log("[HomePage] Navigation action returned:", action);
|
|
|
|
if (action) {
|
|
console.log("[HomePage] Applying navigation:", action);
|
|
actions.setWorkbench(action.workbench);
|
|
if (typeof action.activeFileIndex === "number") {
|
|
setActiveFileIndex(action.activeFileIndex);
|
|
}
|
|
} else {
|
|
console.log("[HomePage] No navigation - staying in current workbench");
|
|
}
|
|
|
|
prevFileCountRef.current = currentCount;
|
|
}, [
|
|
activeFiles.length,
|
|
actions,
|
|
setActiveFileIndex,
|
|
selectedToolKey,
|
|
navigationState.workbench,
|
|
]);
|
|
|
|
const hideToolPanel =
|
|
customWorkbenchViews.find(
|
|
(v) => v.workbenchId === navigationState.workbench,
|
|
)?.hideToolPanel ?? false;
|
|
|
|
const brandAltText = t("home.mobile.brandAlt", "Stirling PDF logo");
|
|
|
|
const handleSelectMobileView = useCallback((view: MobileView) => {
|
|
setActiveMobileView(view);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (isMobile) {
|
|
const container = sliderRef.current;
|
|
if (container) {
|
|
isProgrammaticScroll.current = true;
|
|
const offset = activeMobileView === "tools" ? 0 : container.offsetWidth;
|
|
container.scrollTo({ left: offset, behavior: "smooth" });
|
|
|
|
// Re-enable scroll listener after animation completes
|
|
setTimeout(() => {
|
|
isProgrammaticScroll.current = false;
|
|
}, 500);
|
|
}
|
|
return;
|
|
}
|
|
|
|
setActiveMobileView("tools");
|
|
const container = sliderRef.current;
|
|
if (container) {
|
|
container.scrollTo({ left: 0, behavior: "auto" });
|
|
}
|
|
}, [activeMobileView, isMobile]);
|
|
|
|
useEffect(() => {
|
|
if (!isMobile) return;
|
|
|
|
const container = sliderRef.current;
|
|
if (!container) return;
|
|
|
|
let animationFrame = 0;
|
|
|
|
const handleScroll = () => {
|
|
if (isProgrammaticScroll.current) {
|
|
return;
|
|
}
|
|
|
|
if (animationFrame) {
|
|
cancelAnimationFrame(animationFrame);
|
|
}
|
|
|
|
animationFrame = window.requestAnimationFrame(() => {
|
|
const { scrollLeft, offsetWidth } = container;
|
|
const threshold = offsetWidth / 2;
|
|
const nextView: MobileView =
|
|
scrollLeft >= threshold ? "workbench" : "tools";
|
|
setActiveMobileView((current) =>
|
|
current === nextView ? current : nextView,
|
|
);
|
|
});
|
|
};
|
|
|
|
container.addEventListener("scroll", handleScroll, { passive: true });
|
|
|
|
return () => {
|
|
container.removeEventListener("scroll", handleScroll);
|
|
if (animationFrame) {
|
|
cancelAnimationFrame(animationFrame);
|
|
}
|
|
};
|
|
}, [isMobile]);
|
|
|
|
// Automatically switch to workbench when read mode or multiTool is activated in mobile
|
|
useEffect(() => {
|
|
if (isMobile && (readerMode || selectedToolKey === "multiTool")) {
|
|
setActiveMobileView("workbench");
|
|
}
|
|
}, [isMobile, readerMode, selectedToolKey]);
|
|
|
|
// Automatically switch to workbench slide when a custom workbench (e.g. signing) is active on mobile.
|
|
// hideToolPanel is true for all custom workbenches that take over the full screen.
|
|
useEffect(() => {
|
|
if (isMobile && hideToolPanel) {
|
|
setActiveMobileView("workbench");
|
|
}
|
|
}, [isMobile, hideToolPanel]);
|
|
|
|
// When navigating back to tools view in mobile with a workbench-only tool, show tool picker
|
|
useEffect(() => {
|
|
if (isMobile && activeMobileView === "tools" && selectedTool) {
|
|
// Check if this is a workbench-only tool (has workbench but no component)
|
|
if (selectedTool.workbench && !selectedTool.component) {
|
|
setLeftPanelView("toolPicker");
|
|
}
|
|
}
|
|
}, [isMobile, activeMobileView, selectedTool, setLeftPanelView]);
|
|
|
|
const baseUrl = useBaseUrl();
|
|
|
|
// Update document meta when tool changes
|
|
const appName = config?.appNameNavbar || "Stirling PDF";
|
|
useDocumentMeta({
|
|
title: selectedTool ? `${selectedTool.name} - ${appName}` : appName,
|
|
description:
|
|
selectedTool?.description ||
|
|
t(
|
|
"app.description",
|
|
"The Free Adobe Acrobat alternative (10M+ Downloads)",
|
|
),
|
|
ogTitle: selectedTool ? `${selectedTool.name} - ${appName}` : appName,
|
|
ogDescription:
|
|
selectedTool?.description ||
|
|
t(
|
|
"app.description",
|
|
"The Free Adobe Acrobat alternative (10M+ Downloads)",
|
|
),
|
|
ogImage: selectedToolKey
|
|
? `${baseUrl}/og_images/${selectedToolKey}.png`
|
|
: `${baseUrl}/og_images/home.png`,
|
|
ogUrl: selectedTool ? `${baseUrl}${window.location.pathname}` : baseUrl,
|
|
});
|
|
|
|
// Note: File selection limits are now handled directly by individual tools
|
|
|
|
return (
|
|
<div className="h-screen overflow-hidden">
|
|
<HomePageExtensions />
|
|
{isMobile ? (
|
|
<div className="mobile-layout">
|
|
<div className="mobile-toggle">
|
|
<div className="mobile-header">
|
|
<div className="mobile-brand">
|
|
<LogoIcon className="mobile-brand-icon" />
|
|
<Wordmark alt={brandAltText} className="mobile-brand-text" />
|
|
</div>
|
|
</div>
|
|
<div
|
|
className="mobile-toggle-buttons"
|
|
role="tablist"
|
|
aria-label={t(
|
|
"home.mobile.viewSwitcher",
|
|
"Switch workspace view",
|
|
)}
|
|
>
|
|
<button
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={activeMobileView === "tools"}
|
|
className={`mobile-toggle-button ${activeMobileView === "tools" ? "active" : ""}`}
|
|
onClick={() => handleSelectMobileView("tools")}
|
|
>
|
|
{t("home.mobile.tools", "Tools")}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={activeMobileView === "workbench"}
|
|
className={`mobile-toggle-button ${activeMobileView === "workbench" ? "active" : ""}`}
|
|
onClick={() => handleSelectMobileView("workbench")}
|
|
>
|
|
{t("home.mobile.workspace", "Workspace")}
|
|
</button>
|
|
</div>
|
|
<span className="mobile-toggle-hint">
|
|
{t(
|
|
"home.mobile.swipeHint",
|
|
"Swipe left or right to switch views",
|
|
)}
|
|
</span>
|
|
</div>
|
|
<div ref={sliderRef} className="mobile-slider">
|
|
<div
|
|
className="mobile-slide"
|
|
aria-label={t("home.mobile.toolsSlide", "Tool selection panel")}
|
|
>
|
|
<div className="mobile-slide-content">
|
|
<ToolPanel />
|
|
</div>
|
|
</div>
|
|
<div
|
|
className="mobile-slide"
|
|
aria-label={t("home.mobile.workbenchSlide", "Workspace panel")}
|
|
>
|
|
<div className="mobile-slide-content">
|
|
<div className="flex-1 min-h-0 flex">
|
|
<Workbench />
|
|
<RightRail />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="mobile-bottom-bar">
|
|
<button
|
|
className="mobile-bottom-button"
|
|
aria-label={t("quickAccess.allTools", "Tools")}
|
|
onClick={() => {
|
|
handleBackToTools();
|
|
if (isMobile) {
|
|
setActiveMobileView("tools");
|
|
}
|
|
}}
|
|
>
|
|
<AppsIcon sx={{ fontSize: "1.5rem" }} />
|
|
<span className="mobile-bottom-button-label">
|
|
{t("quickAccess.allTools", "Tools")}
|
|
</span>
|
|
</button>
|
|
{toolAvailability["automate"]?.available !== false && (
|
|
<button
|
|
className="mobile-bottom-button"
|
|
aria-label={t("quickAccess.automate", "Automate")}
|
|
onClick={() => {
|
|
handleToolSelect("automate");
|
|
if (isMobile) {
|
|
setActiveMobileView("tools");
|
|
}
|
|
}}
|
|
>
|
|
<LocalIcon
|
|
icon="automation-outline"
|
|
width="1.5rem"
|
|
height="1.5rem"
|
|
/>
|
|
<span className="mobile-bottom-button-label">
|
|
{t("quickAccess.automate", "Automate")}
|
|
</span>
|
|
</button>
|
|
)}
|
|
<button
|
|
className="mobile-bottom-button"
|
|
aria-label={t("home.mobile.openFiles", "Open files")}
|
|
onClick={() => openFilesModal()}
|
|
>
|
|
<LocalIcon icon="folder-rounded" width="1.5rem" height="1.5rem" />
|
|
<span className="mobile-bottom-button-label">
|
|
{t("quickAccess.files", "Files")}
|
|
</span>
|
|
</button>
|
|
<button
|
|
className="mobile-bottom-button"
|
|
aria-label={t("quickAccess.config", "Config")}
|
|
onClick={() => setConfigModalOpen(true)}
|
|
>
|
|
<LocalIcon
|
|
icon="settings-rounded"
|
|
width="1.5rem"
|
|
height="1.5rem"
|
|
/>
|
|
<span className="mobile-bottom-button-label">
|
|
{t("quickAccess.config", "Config")}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
<FileManager selectedTool={selectedTool as any /* FIX ME */} />
|
|
<AppConfigModal
|
|
opened={configModalOpen}
|
|
onClose={() => setConfigModalOpen(false)}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<Group align="flex-start" gap={0} h="100%" className="flex-nowrap flex">
|
|
<QuickAccessBar ref={quickAccessRef} />
|
|
{!hideToolPanel && <ToolPanel />}
|
|
<Workbench />
|
|
<RightRail />
|
|
<FileManager selectedTool={selectedTool as any /* FIX ME */} />
|
|
</Group>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|