mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: James Brunton <[email protected]>
287 lines
11 KiB
TypeScript
287 lines
11 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
|
|
import { Group, useMantineColorScheme } from "@mantine/core";
|
|
import { useSidebarContext } from "@app/contexts/SidebarContext";
|
|
import { useDocumentMeta } from "@app/hooks/useDocumentMeta";
|
|
import { BASE_PATH } from "@app/constants/app";
|
|
import { useBaseUrl } from "@app/hooks/useBaseUrl";
|
|
import { useMediaQuery } from "@mantine/hooks";
|
|
import { useAppConfig } from "@app/contexts/AppConfigContext";
|
|
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/AppConfigModal";
|
|
import ToolPanelModePrompt from "@app/components/tools/ToolPanelModePrompt";
|
|
import AdminAnalyticsChoiceModal from "@app/components/shared/AdminAnalyticsChoiceModal";
|
|
|
|
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,
|
|
} = useToolWorkflow();
|
|
|
|
const { openFilesModal } = useFilesModalContext();
|
|
const { colorScheme } = useMantineColorScheme();
|
|
const { config } = useAppConfig();
|
|
const isMobile = useMediaQuery("(max-width: 1024px)");
|
|
const sliderRef = useRef<HTMLDivElement | null>(null);
|
|
const [activeMobileView, setActiveMobileView] = useState<MobileView>("tools");
|
|
const isProgrammaticScroll = useRef(false);
|
|
const [configModalOpen, setConfigModalOpen] = useState(false);
|
|
const [showAnalyticsModal, setShowAnalyticsModal] = useState(false);
|
|
|
|
// Show admin analytics choice modal if analytics settings not configured
|
|
useEffect(() => {
|
|
if (config && config.enableAnalytics === null) {
|
|
setShowAnalyticsModal(true);
|
|
}
|
|
}, [config]);
|
|
|
|
const brandAltText = t("home.mobile.brandAlt", "Stirling PDF logo");
|
|
const brandIconSrc = `${BASE_PATH}/branding/StirlingPDFLogoNoText${
|
|
colorScheme === "dark" ? "Dark" : "Light"
|
|
}.svg`;
|
|
const brandTextSrc = `${BASE_PATH}/branding/StirlingPDFLogo${
|
|
colorScheme === "dark" ? "White" : "Black"
|
|
}Text.svg`;
|
|
|
|
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]);
|
|
|
|
// 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">
|
|
<AdminAnalyticsChoiceModal
|
|
opened={showAnalyticsModal}
|
|
onClose={() => setShowAnalyticsModal(false)}
|
|
/>
|
|
<ToolPanelModePrompt />
|
|
{isMobile ? (
|
|
<div className="mobile-layout">
|
|
<div className="mobile-toggle">
|
|
<div className="mobile-header">
|
|
<div className="mobile-brand">
|
|
<img src={brandIconSrc} alt="" className="mobile-brand-icon" />
|
|
<img src={brandTextSrc} 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', 'All Tools')}
|
|
onClick={() => {
|
|
handleBackToTools();
|
|
if (isMobile) {
|
|
setActiveMobileView('tools');
|
|
}
|
|
}}
|
|
>
|
|
<AppsIcon sx={{ fontSize: '1.5rem' }} />
|
|
<span className="mobile-bottom-button-label">{t('quickAccess.allTools', 'All Tools')}</span>
|
|
</button>
|
|
<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} />
|
|
<ToolPanel />
|
|
<Workbench />
|
|
<RightRail />
|
|
<FileManager selectedTool={selectedTool as any /* FIX ME */} />
|
|
</Group>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|