Skip to content

Commit 32c4a51

Browse files
committed
frontend/latex+pdf: PDF dark mode, with a quick toggle to see the original without exiting dark mode
1 parent 5697cf9 commit 32c4a51

File tree

13 files changed

+233
-94
lines changed

13 files changed

+233
-94
lines changed

src/packages/frontend/account/dark-mode.ts

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
* License: MS-RSL – see LICENSE.md for details
44
*/
55

6-
import { throttle, isEqual, debounce } from "lodash";
6+
import { isEqual, debounce } from "lodash";
77

88
import { DARK_MODE_DEFAULTS } from "@cocalc/util/db-schema/accounts";
99
import { AccountStore } from "./store";
1010

1111
export const DARK_MODE_KEYS = ["brightness", "contrast", "sepia"] as const;
1212

13+
// Icon unicode character for dark mode toggle (◑ - circle with right half black)
14+
export const DARK_MODE_ICON = 0x25d1;
15+
1316
type Config = Record<(typeof DARK_MODE_KEYS)[number], number>;
1417

1518
export const DARK_MODE_MINS: Config = {
@@ -61,28 +64,35 @@ let last_config: Config | undefined = undefined;
6164
export function init_dark_mode(account_store: AccountStore): void {
6265
account_store.on(
6366
"change",
64-
debounce(async () => {
65-
const dark_mode = !!account_store.getIn(["other_settings", "dark_mode"]);
66-
currentDarkMode = dark_mode;
67-
const config = get_dark_mode_config(
68-
account_store.get("other_settings")?.toJS(),
69-
);
70-
if (
71-
dark_mode == last_dark_mode &&
72-
(!dark_mode || isEqual(last_config, config))
73-
) {
74-
return;
75-
}
76-
const { enable, disable } = await import("darkreader");
77-
last_dark_mode = dark_mode;
78-
last_config = config;
79-
if (dark_mode) {
80-
disable();
81-
enable(config);
82-
} else {
83-
disable();
84-
}
85-
}, 1000, {trailing: true, leading: false}),
67+
debounce(
68+
async () => {
69+
const dark_mode = !!account_store.getIn([
70+
"other_settings",
71+
"dark_mode",
72+
]);
73+
currentDarkMode = dark_mode;
74+
const config = get_dark_mode_config(
75+
account_store.get("other_settings")?.toJS(),
76+
);
77+
if (
78+
dark_mode == last_dark_mode &&
79+
(!dark_mode || isEqual(last_config, config))
80+
) {
81+
return;
82+
}
83+
const { enable, disable } = await import("darkreader");
84+
last_dark_mode = dark_mode;
85+
last_config = config;
86+
if (dark_mode) {
87+
disable();
88+
enable(config);
89+
} else {
90+
disable();
91+
}
92+
},
93+
1000,
94+
{ trailing: true, leading: false },
95+
),
8696
);
8797
}
8898

src/packages/frontend/account/other-settings.tsx

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { webapp_client } from "@cocalc/frontend/webapp-client";
4444
import { DEFAULT_NEW_FILENAMES, NEW_FILENAMES } from "@cocalc/util/db-schema";
4545
import { OTHER_SETTINGS_REPLY_ENGLISH_KEY } from "@cocalc/util/i18n/const";
4646
import {
47+
DARK_MODE_ICON,
4748
DARK_MODE_KEYS,
4849
DARK_MODE_MINS,
4950
get_dark_mode_config,
@@ -104,12 +105,11 @@ export function OtherSettings(props: Readonly<Props>): React.JSX.Element {
104105
// Debounced version for dark mode sliders to reduce CPU usage
105106
const on_change_dark_mode = useMemo(
106107
() =>
107-
debounce(
108-
(name: string, value: any) => on_change(name, value),
109-
50,
110-
{ trailing: true, leading: false }
111-
),
112-
[]
108+
debounce((name: string, value: any) => on_change(name, value), 50, {
109+
trailing: true,
110+
leading: false,
111+
}),
112+
[],
113113
);
114114

115115
function toggle_global_banner(val: boolean): void {
@@ -427,10 +427,15 @@ export function OtherSettings(props: Readonly<Props>): React.JSX.Element {
427427
{checked ? (
428428
<Card
429429
size="small"
430-
title={intl.formatMessage({
431-
id: "account.other-settings.theme.dark_mode.configuration",
432-
defaultMessage: "Dark Mode Configuration",
433-
})}
430+
title={
431+
<>
432+
<Icon unicode={DARK_MODE_ICON} />{" "}
433+
{intl.formatMessage({
434+
id: "account.other-settings.theme.dark_mode.configuration",
435+
defaultMessage: "Dark Mode Configuration",
436+
})}
437+
</>
438+
}
434439
>
435440
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
436441
{DARK_MODE_KEYS.map((key) => (
@@ -456,7 +461,10 @@ export function OtherSettings(props: Readonly<Props>): React.JSX.Element {
456461
<Button
457462
size="small"
458463
onClick={() =>
459-
on_change_dark_mode(`dark_mode_${key}`, DARK_MODE_DEFAULTS[key])
464+
on_change_dark_mode(
465+
`dark_mode_${key}`,
466+
DARK_MODE_DEFAULTS[key],
467+
)
460468
}
461469
>
462470
{intl.formatMessage(labels.reset)}

src/packages/frontend/frame-editors/code-editor/actions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ export interface CodeEditorState {
164164
derived_file_types: iSet<string>;
165165
visible: boolean;
166166
switch_to_files: string[];
167+
pdf_dark_mode_disabled?: { [id: string]: boolean };
167168
}
168169

169170
export class Actions<
@@ -1370,6 +1371,12 @@ export class Actions<
13701371
this.change_font_size(undefined, id, zoom);
13711372
}
13721373

1374+
toggle_pdf_dark_mode(id: string): void {
1375+
const next = this.store.get("pdf_dark_mode_disabled")?.toJS() ?? {};
1376+
next[id] = !(next[id] ?? false);
1377+
this.setState({ pdf_dark_mode_disabled: next });
1378+
}
1379+
13731380
/* zoom: 1=100%, 1.5=150%, ...*/
13741381
change_font_size(delta?: number, id?: string, zoom?: number): void {
13751382
if (delta == null && zoom == null) return;

src/packages/frontend/frame-editors/frame-tree/commands/generic-commands.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { debounce } from "lodash";
1010
import { useEffect, useRef } from "react";
1111
import { defineMessage, IntlShape, useIntl } from "react-intl";
1212

13+
import { DARK_MODE_ICON } from "@cocalc/frontend/account/dark-mode";
1314
import { set_account_table } from "@cocalc/frontend/account/util";
1415
import { redux } from "@cocalc/frontend/app-framework";
1516
import { Icon } from "@cocalc/frontend/components";
@@ -331,6 +332,24 @@ addCommands({
331332
};
332333
}),
333334
},
335+
toggle_pdf_dark_mode: {
336+
pos: 6,
337+
group: "zoom",
338+
stayOpenOnClick: true,
339+
title: editor.toggle_pdf_dark_mode_title,
340+
label: editor.toggle_pdf_dark_mode_label,
341+
icon: () => <Icon unicode={DARK_MODE_ICON} />,
342+
isVisible: () => {
343+
const other_settings = redux
344+
.getStore("account")
345+
.get("other_settings")
346+
?.toJS();
347+
return other_settings?.dark_mode ?? false;
348+
},
349+
onClick: ({ props }) => {
350+
props.actions.toggle_pdf_dark_mode?.(props.id);
351+
},
352+
},
334353
scrollToTop: {
335354
group: "scroll",
336355
pos: 0,

src/packages/frontend/frame-editors/latex-editor/editor.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,11 @@ const output: EditorDescription = {
116116
"force_build",
117117
"clean",
118118
"stop_build",
119-
"sync",
120119
"decrease_font_size",
121120
"increase_font_size",
122121
"zoom_page_width",
123122
"zoom_page_height",
124123
"set_zoom",
125-
"print",
126-
"download_pdf",
127124
]),
128125
} as const;
129126

src/packages/frontend/frame-editors/latex-editor/output-control-build.tsx

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import type { MenuProps } from "antd";
1212
import { Dropdown } from "antd";
1313
import { useIntl } from "react-intl";
1414

15+
import { Button as BSButton } from "@cocalc/frontend/antd-bootstrap";
16+
import { DARK_MODE_ICON } from "@cocalc/frontend/account/dark-mode";
1517
import { set_account_table } from "@cocalc/frontend/account/util";
16-
import { useRedux } from "@cocalc/frontend/app-framework";
18+
import { useRedux, useTypedRedux } from "@cocalc/frontend/app-framework";
1719
import { Icon } from "@cocalc/frontend/components";
1820
import { COMMANDS } from "@cocalc/frontend/frame-editors/frame-tree/commands";
1921
import {
@@ -38,6 +40,17 @@ export function BuildControls({ actions, id, narrow }: BuildControlsProps) {
3840
const buildOnSave =
3941
useRedux(["account", "editor_settings", "build_on_save"]) ?? false;
4042

43+
// Check if global dark mode is enabled
44+
const other_settings = useTypedRedux("account", "other_settings");
45+
const isDarkMode = other_settings?.get("dark_mode") ?? false;
46+
47+
// Get PDF dark mode disabled state from Redux store
48+
const pdfDarkModeDisabledMap = useRedux(
49+
actions.name,
50+
"pdf_dark_mode_disabled",
51+
);
52+
const pdfDarkModeDisabled = pdfDarkModeDisabledMap?.get?.(id) ?? false;
53+
4154
const handleBuild = () => {
4255
actions.build();
4356
};
@@ -102,16 +115,31 @@ export function BuildControls({ actions, id, narrow }: BuildControlsProps) {
102115
];
103116

104117
return (
105-
<Dropdown.Button
106-
type="primary"
107-
size="small"
108-
icon={<Icon name="caret-down" />}
109-
menu={{ items: buildMenuItems }}
110-
trigger={["click"]}
111-
onClick={handleBuild}
112-
>
113-
<Icon name="play-circle" />
114-
{!narrow && intl.formatMessage(editor.build_control_and_log_title_short)}
115-
</Dropdown.Button>
118+
<>
119+
<Dropdown.Button
120+
type="primary"
121+
size="small"
122+
icon={<Icon name="caret-down" />}
123+
menu={{ items: buildMenuItems }}
124+
trigger={["click"]}
125+
onClick={handleBuild}
126+
>
127+
<Icon name="play-circle" />
128+
{!narrow &&
129+
intl.formatMessage(editor.build_control_and_log_title_short)}
130+
</Dropdown.Button>
131+
132+
{/* Dark mode toggle - only shown when global dark mode is enabled */}
133+
{isDarkMode && (
134+
<BSButton
135+
bsSize="xsmall"
136+
active={pdfDarkModeDisabled}
137+
onClick={() => actions.toggle_pdf_dark_mode(id)}
138+
title={intl.formatMessage(editor.toggle_pdf_dark_mode_title)}
139+
>
140+
<Icon unicode={DARK_MODE_ICON} />
141+
</BSButton>
142+
)}
143+
</>
116144
);
117145
}

src/packages/frontend/frame-editors/latex-editor/output.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ export function Output(props: OutputProps) {
389389
label: (
390390
<span style={LABEL_STYLE}>
391391
<Icon name="align-right" />
392-
{intl.formatMessage(editor.table_of_contents_name)}
392+
{intl.formatMessage(editor.table_of_contents_short)}
393393
</span>
394394
),
395395
children: (

src/packages/frontend/frame-editors/latex-editor/pdfjs-annotation.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export default function AnnotationLayer({
106106
background: "yellow",
107107
border: "1px solid grey",
108108
boxShadow: "3px 3px 3px 0px #ddd",
109+
zIndex: 1, // without that, in dark-mode it stays hidden
109110
}}
110111
/>
111112
);
@@ -137,7 +138,7 @@ export default function AnnotationLayer({
137138
}
138139

139140
// Note: this "annotation" in the onClick below is the right one because we use "let"
140-
// *inside* the for loop above -- I'm not making the typical closure/scopying mistake.
141+
// *inside* the for loop above -- I'm not making the typical closure/scoping mistake.
141142
const elt = (
142143
<div
143144
onClick={() => clickAnnotation(annotation)}

0 commit comments

Comments
 (0)