feat(settings): add default startup view and reader zoom preferences (#6073)

## Description of Changes

Adds two new user preferences to the General settings panel, addressing
#5908.

**Default view on launch** - a segmented control (Tools / Reader /
Automate) that controls which left-column tab is active when the app
starts. Previously the app always opened on the Tools tab with no way to
change this. Users who spend most of their time reading PDFs had to
manually switch to the Reader tab on every launch.

**Default reader zoom** - a dropdown (Auto / Fit width / Fit page /
50%–200%) that sets the initial zoom level whenever a PDF is opened in
the reader. Previously the app always applied an automatic
fit-to-viewport calculation.

Both settings are non-breaking. The defaults (`Tools` and `Auto`)
reproduce the existing behaviour exactly, so existing users see no
difference until they change a preference.

### What changed
- `preferencesService.ts` - added `StartupView` and `ViewerZoomSetting`
types plus the two new fields to `UserPreferences` with safe defaults
- `ToolWorkflowContext.tsx` - one-time startup effect that navigates to
the preferred tab on first render (mirrors the existing
`defaultToolPanelMode` sync pattern)
- `ZoomAPIBridge.tsx` - respects the zoom preference before falling back
to auto-zoom logic when a document loads
- `GeneralSection.tsx` - two new controls added below "Default tool
picker mode"; the Select uses `comboboxProps={{ withinPortal: true }}`
so the dropdown renders above the settings modal
- `en-GB/translation.toml` - new keys for labels, descriptions, and
option values

Closes #5908 

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [x] 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)
- [x] I have performed a self-review of my own code
- [x] 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)
- [x] 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)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [x] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)
<img width="1023" height="747" alt="Screenshot 2026-04-05 185718"
src="https://github.com/user-attachments/assets/6a8bc35a-d813-4ab8-b303-55bdce747a6a"
/>
<img width="1026" height="755" alt="Screenshot 2026-04-05 185620"
src="https://github.com/user-attachments/assets/d2c45134-ed32-4332-a193-1a96837ba2a3"
/>


### Testing (if applicable)

- [x] 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.
This commit is contained in:
Vibe Stack
2026-04-09 06:30:19 -04:00
committed by GitHub
parent cc1604a802
commit a5b259b453
5 changed files with 118 additions and 0 deletions

View File

@@ -6638,9 +6638,13 @@ defaultPdfEditorActive = "Stirling PDF is your default PDF editor"
defaultPdfEditorChecking = "Checking..."
defaultPdfEditorInactive = "Another application is set as default"
defaultPdfEditorSet = "Already Default"
defaultStartupView = "Default view on launch"
defaultStartupViewDescription = "Choose which tab is active in the left column when the app starts"
defaultToolPickerMode = "Default tool picker mode"
defaultToolPickerModeDescription = "Choose whether the tool picker opens in fullscreen or sidebar by default"
description = "Configure general application preferences."
defaultViewerZoom = "Default reader zoom"
defaultViewerZoomDescription = "Set the default zoom level when opening PDFs in the reader"
hideUnavailableConversions = "Hide unavailable conversions"
hideUnavailableConversionsDescription = "Remove disabled conversion options in the Convert tool instead of showing them greyed out."
hideUnavailableTools = "Hide unavailable tools"
@@ -6663,6 +6667,16 @@ title = "For System Administrators"
fullscreen = "Fullscreen"
sidebar = "Sidebar"
[settings.general.startupView]
automate = "Automate"
read = "Reader"
tools = "Tools"
[settings.general.zoomLevel]
auto = "Auto"
fitPage = "Fit page"
fitWidth = "Fit width"
[settings.general.updates]
checkForUpdates = "Check for Updates"
currentBackendVersion = "Current Backend Version"

View File

@@ -7,6 +7,7 @@ import {
Tooltip,
NumberInput,
SegmentedControl,
Select,
Code,
Group,
Anchor,
@@ -19,6 +20,8 @@ import { useTranslation } from "react-i18next";
import { usePreferences } from "@app/contexts/PreferencesContext";
import { useAppConfig } from "@app/contexts/AppConfigContext";
import type { ToolPanelMode } from "@app/constants/toolPanel";
import type { StartupView, ViewerZoomSetting } from "@app/services/preferencesService";
import { Z_INDEX_OVER_CONFIG_MODAL } from "@app/styles/zIndex";
import LocalIcon from "@app/components/shared/LocalIcon";
import { updateService, UpdateSummary } from "@app/services/updateService";
import UpdateModal from "@app/components/shared/UpdateModal";
@@ -293,6 +296,61 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false, hide
]}
/>
</div>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<div>
<Text fw={500} size="sm">
{t("settings.general.defaultStartupView", "Default view on launch")}
</Text>
<Text size="xs" c="dimmed" mt={4}>
{t(
"settings.general.defaultStartupViewDescription",
"Choose which tab is active in the left column when the app starts",
)}
</Text>
</div>
<SegmentedControl
value={preferences.defaultStartupView}
onChange={(val: string) => updatePreference("defaultStartupView", val as StartupView)}
data={[
{ label: t("settings.general.startupView.tools", "Tools"), value: "tools" },
{ label: t("settings.general.startupView.read", "Reader"), value: "read" },
{ label: t("settings.general.startupView.automate", "Automate"), value: "automate" },
]}
/>
</div>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<div>
<Text fw={500} size="sm">
{t("settings.general.defaultViewerZoom", "Default reader zoom")}
</Text>
<Text size="xs" c="dimmed" mt={4}>
{t(
"settings.general.defaultViewerZoomDescription",
"Set the default zoom level when opening PDFs in the reader",
)}
</Text>
</div>
<Select
value={preferences.defaultViewerZoom}
onChange={(val: string | null) => {
if (val) updatePreference("defaultViewerZoom", val as ViewerZoomSetting);
}}
data={[
{ label: t("settings.general.zoomLevel.auto", "Auto"), value: "auto" },
{ label: t("settings.general.zoomLevel.fitWidth", "Fit width"), value: "fitWidth" },
{ label: t("settings.general.zoomLevel.fitPage", "Fit page"), value: "fitPage" },
{ label: "50%", value: "50" },
{ label: "75%", value: "75" },
{ label: "100%", value: "100" },
{ label: "125%", value: "125" },
{ label: "150%", value: "150" },
{ label: "200%", value: "200" },
]}
style={{ width: 140 }}
allowDeselect={false}
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
/>
</div>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<div>
<Text fw={500} size="sm">

View File

@@ -12,6 +12,7 @@ import {
} from '@app/utils/viewerZoom';
import { getFirstPageAspectRatioFromStub } from '@app/utils/pageMetadata';
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
import { preferencesService, type ViewerZoomSetting } from '@app/services/preferencesService';
/**
* Connects the PDF zoom plugin to the shared ViewerContext.
@@ -155,6 +156,21 @@ function ZoomAPIBridgeInner({ documentId }: { documentId: string }) {
return;
}
// Check user preference for default viewer zoom
const zoomPref: ViewerZoomSetting = preferencesService.getPreference('defaultViewerZoom');
if (zoomPref !== 'auto') {
if (zoomPref === 'fitWidth') {
applyTrackedZoom(ZoomMode.FitWidth, fitWidthZoom);
} else if (zoomPref === 'fitPage') {
applyTrackedZoom(ZoomMode.FitPage, fitWidthZoom);
} else {
// Numeric zoom: '50', '75', '100', '125', '150', '200'
const numericZoom = parseInt(zoomPref, 10) / 100;
applyTrackedZoom(numericZoom, numericZoom);
}
return;
}
const viewportWidth = window.innerWidth ?? 0;
const viewportHeight = window.innerHeight ?? 0;

View File

@@ -258,6 +258,28 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
}
}, [preferences.defaultToolPanelMode, state.toolPanelMode]);
// Apply default startup view preference on initial load.
// This runs once to navigate to the user's preferred tab (read/automate)
// instead of always starting on the tools tab.
const hasAppliedStartupView = React.useRef(false);
useEffect(() => {
if (hasAppliedStartupView.current) return;
const startupView = preferences.defaultStartupView;
if (startupView === 'read') {
hasAppliedStartupView.current = true;
setReaderMode(true);
actions.setSelectedTool('read');
} else if (startupView === 'automate') {
hasAppliedStartupView.current = true;
actions.setSelectedTool('automate');
setLeftPanelView('toolContent');
}
// 'tools' is the default — no action needed
if (startupView === 'tools') {
hasAppliedStartupView.current = true;
}
}, [preferences.defaultStartupView, actions, setReaderMode, setLeftPanelView]);
// Tool reset methods
const registerToolReset = useCallback((toolId: string, resetFunction: () => void) => {
setToolResetFunctions(prev => ({ ...prev, [toolId]: resetFunction }));

View File

@@ -5,10 +5,16 @@ export type LogoVariant = 'modern' | 'classic';
export type PdfRenderMode = 'normal' | 'dark' | 'sepia';
export type StartupView = 'tools' | 'read' | 'automate';
export type ViewerZoomSetting = 'auto' | 'fitWidth' | 'fitPage' | '50' | '75' | '100' | '125' | '150' | '200';
export interface UserPreferences {
autoUnzip: boolean;
autoUnzipFileLimit: number;
defaultToolPanelMode: ToolPanelMode;
defaultStartupView: StartupView;
defaultViewerZoom: ViewerZoomSetting;
theme: ThemeMode;
toolPanelModePromptSeen: boolean;
hasSelectedToolPanelMode: boolean;
@@ -26,6 +32,8 @@ export const DEFAULT_PREFERENCES: UserPreferences = {
autoUnzip: true,
autoUnzipFileLimit: 4,
defaultToolPanelMode: DEFAULT_TOOL_PANEL_MODE,
defaultStartupView: 'tools',
defaultViewerZoom: 'auto',
theme: getSystemTheme(),
toolPanelModePromptSeen: false,
hasSelectedToolPanelMode: false,