Skip to content

Commit f545851

Browse files
committed
Add support for custom themes
1 parent ac5a7f9 commit f545851

File tree

3 files changed

+45
-7
lines changed

3 files changed

+45
-7
lines changed

src/components/settings/settings-page.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { SubscriptionPlans } from '@httptoolkit/accounts';
1010
import { WithInjected } from '../../types';
1111
import { styled, Theme, ThemeName } from '../../styles';
1212
import { Icon, WarningIcon } from '../../icons';
13+
import { uploadFile } from '../../util/ui';
1314

1415
import { AccountStore } from '../../model/account/account-store';
1516
import { UiStore } from '../../model/ui/ui-store';
@@ -290,14 +291,23 @@ class SettingsPage extends React.Component<SettingsPageProps> {
290291
</header>
291292
<TabbedOptionsContainer>
292293
<TabsContainer
293-
onClick={(value: ThemeName | 'automatic' | Theme) => uiStore.setTheme(value)}
294-
isSelected={(value: ThemeName | 'automatic' | Theme) => {
295-
if (typeof value === 'string') {
296-
return uiStore.themeName === value;
294+
onClick={async (value: ThemeName | 'automatic' | 'custom') => {
295+
if (value === 'custom') {
296+
const themeFile = await uploadFile('text', ['.htktheme', '.htk-theme', '.json']);
297+
if (!themeFile) return;
298+
try {
299+
const customTheme = uiStore.buildCustomTheme(themeFile);
300+
uiStore.setTheme(customTheme);
301+
} catch (e: any) {
302+
alert(e.message || e);
303+
}
297304
} else {
298-
return _.isEqual(value, uiStore.theme);
305+
uiStore.setTheme(value);
299306
}
300307
}}
308+
isSelected={(value: ThemeName | 'automatic' | 'custom') =>
309+
uiStore.themeName === value
310+
}
301311
>
302312
<Tab
303313
icon='MagicWand'
@@ -323,6 +333,12 @@ class SettingsPage extends React.Component<SettingsPageProps> {
323333
>
324334
High Contrast
325335
</Tab>
336+
<Tab
337+
icon='Swatches'
338+
value='custom'
339+
>
340+
Custom
341+
</Tab>
326342
</TabsContainer>
327343
<ThemeColors>
328344
<ThemeColorBlock themeColor='mainColor' />

src/icons.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import {
2222
MagicWand,
2323
Sun,
2424
Moon,
25-
CircleHalf
25+
CircleHalf,
26+
Swatches
2627
} from '@phosphor-icons/react';
2728

2829
export type IconKey = keyof typeof Icons;
@@ -53,7 +54,8 @@ const Icons = {
5354
MagicWand,
5455
Sun,
5556
Moon,
56-
CircleHalf
57+
CircleHalf,
58+
Swatches
5759
} as const;
5860

5961
// Import required FA icons:

src/model/ui/ui-store.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
ContextMenuOption,
1717
buildNativeContextMenuItems
1818
} from './context-menu';
19+
import { tryParseJson } from '../../util';
1920

2021
const VIEW_CARD_KEYS = [
2122
'api',
@@ -93,6 +94,11 @@ const SETTINGS_CARD_KEYS =[
9394
] as const;
9495
type SettingsCardKey = typeof SETTINGS_CARD_KEYS[number];
9596

97+
type CustomTheme = Partial<Theme> & {
98+
name: string;
99+
extends: ThemeName;
100+
};
101+
96102
export class UiStore {
97103

98104
constructor(
@@ -146,6 +152,20 @@ export class UiStore {
146152
}
147153
}
148154

155+
buildCustomTheme(themeFile: string) {
156+
const themeData: Partial<CustomTheme> | undefined = tryParseJson(themeFile);
157+
if (!themeData) throw new Error("Could not parse theme JSON");
158+
159+
if (!themeData.name) throw new Error('Theme must contain a `name` field');
160+
if (!themeData.extends) throw new Error('Theme must contain an `extends` field with a built-in theme name (dark/light/high-contrast)');
161+
162+
const baseTheme = Themes[themeData.extends];
163+
return {
164+
...baseTheme,
165+
...themeData
166+
} as Theme;
167+
}
168+
149169
@persist @observable
150170
private _themeName: ThemeName | 'automatic' | 'custom' = 'automatic';
151171

0 commit comments

Comments
 (0)