style(settings): fold the Happ settings into four tabs

Seven tabs, several holding one or two settings, made the page hard to
scan. The encrypted-links switch moves above the tabs under
auto-detection, the colour profile joins the banners under Appearance &
Theme, and Android per-app proxy joins Network & TUN Engine. The QR
modal's settings link no longer needs a happTab selector, and the three
orphaned tab-label keys are dropped from every locale.
This commit is contained in:
MHSanaei
2026-09-27 16:06:42 +02:00
parent a33b2341e9
commit 8f47b53879
18 changed files with 103 additions and 209 deletions
+1 -1
View File
@@ -50,7 +50,7 @@ type QrVariant = 'standard' | 'happ';
type HappError = 'too_long' | 'unavailable' | null;
const HAPP_CRYPT5_PREFIX = 'happ://crypt5/';
const HAPP_SETTINGS_PATH = '/settings?subscriptionTab=happ&happTab=links#subscription';
const HAPP_SETTINGS_PATH = '/settings?subscriptionTab=happ#subscription';
// QrPanel encodes at error level L; QR version 40 holds 2953 UTF-8 bytes at that level.
const HAPP_QR_MAX_BYTES = 2953;
const UTF8_ENCODER = new TextEncoder();
@@ -6,9 +6,6 @@ import {
BuildOutlined,
CloudSyncOutlined,
DesktopOutlined,
LinkOutlined,
MobileOutlined,
NotificationOutlined,
ThunderboltOutlined,
} from '@ant-design/icons';
import type { AllSetting } from '@/models/setting';
@@ -22,7 +19,6 @@ interface HappSettingsContentProps {
updateSetting: (patch: Partial<AllSetting>) => void;
isMobile: boolean;
remoteSourceBadge: (val: string) => React.ReactNode;
defaultActiveTab?: 'routing' | 'links';
}
export default function HappSettingsContent({
@@ -30,7 +26,6 @@ export default function HappSettingsContent({
updateSetting,
isMobile,
remoteSourceBadge,
defaultActiveTab = 'routing',
}: HappSettingsContentProps) {
const { t } = useTranslation();
// Generator choices stay local until Apply updates the draft; page Save persists it.
@@ -65,17 +60,27 @@ export default function HappSettingsContent({
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.happLinkEnable')}
description={t('pages.settings.happLinkEnableDesc')}
>
<Switch
checked={allSetting.happLinkEnable}
onChange={(v) => updateSetting({ happLinkEnable: v })}
/>
</SettingListItem>
<Tabs
type="card"
size="small"
defaultActiveKey={defaultActiveTab}
items={[
{
key: 'routing',
label: (
<span>
<BranchesOutlined /> {!isMobile && t('pages.settings.subHappGroupRouting')}
</span>
label: catTabLabel(
<BranchesOutlined />,
t('pages.settings.subHappGroupRouting'),
isMobile,
),
children: (
<>
@@ -173,27 +178,11 @@ export default function HappSettingsContent({
),
},
{
key: 'links',
label: catTabLabel(<LinkOutlined />, t('pages.settings.subHappGroupLinks'), isMobile),
children: (
<SettingListItem
paddings="small"
title={t('pages.settings.happLinkEnable')}
description={t('pages.settings.happLinkEnableDesc')}
>
<Switch
checked={allSetting.happLinkEnable}
onChange={(v) => updateSetting({ happLinkEnable: v })}
/>
</SettingListItem>
),
},
{
key: 'banners',
label: (
<span>
<NotificationOutlined /> {!isMobile && t('pages.settings.subHappGroupBanners')}
</span>
key: 'appearance',
label: catTabLabel(
<DesktopOutlined />,
t('pages.settings.subHappGroupThemes'),
isMobile,
),
children: (
<>
@@ -285,15 +274,59 @@ export default function HappSettingsContent({
onChange={(v) => updateSetting({ subHappNotificationExpire: v })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappColorProfile')}
description={t('pages.settings.subHappColorProfileDesc')}
>
<Space orientation="vertical" style={{ width: '100%' }}>
<Input
value={allSetting.subHappColorProfile}
placeholder='{"serverRowBackgroundColor":"#21003D67"} or resetcolors'
onChange={(e) => updateSetting({ subHappColorProfile: e.target.value })}
/>
<Space wrap size="small">
<Button
size="small"
onClick={() => updateSetting({ subHappColorProfile: 'resetcolors' })}
>
{t('reset')}
</Button>
<Button
size="small"
onClick={() =>
updateSetting({
subHappColorProfile:
'{"serverRowBackgroundColor":"#21003D67","cardBackgroundColor":"#120023B3"}',
})
}
>
Violet
</Button>
<Button
size="small"
onClick={() =>
updateSetting({
subHappColorProfile:
'{"serverRowBackgroundColor":"#002B3667","cardBackgroundColor":"#001F27B3"}',
})
}
>
Turquoise
</Button>
</Space>
</Space>
</SettingListItem>
</>
),
},
{
key: 'network',
label: (
<span>
<ThunderboltOutlined /> {!isMobile && t('pages.settings.subHappGroupNetwork')}
</span>
label: catTabLabel(
<ThunderboltOutlined />,
t('pages.settings.subHappGroupNetwork'),
isMobile,
),
children: (
<>
@@ -422,70 +455,45 @@ export default function HappSettingsContent({
]}
/>
</SettingListItem>
</>
),
},
{
key: 'themes',
label: (
<span>
<DesktopOutlined /> {!isMobile && t('pages.settings.subHappGroupThemes')}
</span>
),
children: (
<>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappColorProfile')}
description={t('pages.settings.subHappColorProfileDesc')}
title={t('pages.settings.subHappPerAppMode')}
description={t('pages.settings.subHappPerAppModeDesc')}
>
<Space orientation="vertical" style={{ width: '100%' }}>
<Input
value={allSetting.subHappColorProfile}
placeholder='{"serverRowBackgroundColor":"#21003D67"} or resetcolors'
onChange={(e) => updateSetting({ subHappColorProfile: e.target.value })}
/>
<Space wrap size="small">
<Button
size="small"
onClick={() => updateSetting({ subHappColorProfile: 'resetcolors' })}
>
{t('reset')}
</Button>
<Button
size="small"
onClick={() =>
updateSetting({
subHappColorProfile:
'{"serverRowBackgroundColor":"#21003D67","cardBackgroundColor":"#120023B3"}',
})
}
>
Violet
</Button>
<Button
size="small"
onClick={() =>
updateSetting({
subHappColorProfile:
'{"serverRowBackgroundColor":"#002B3667","cardBackgroundColor":"#001F27B3"}',
})
}
>
Turquoise
</Button>
</Space>
</Space>
<Select
value={allSetting.subHappPerAppMode || 'off'}
style={{ width: '100%' }}
onChange={(v) => updateSetting({ subHappPerAppMode: v })}
options={[
{ value: 'off', label: t('pages.settings.subHappPerAppOff') },
{ value: 'on', label: t('pages.settings.subHappPerAppOn') },
{ value: 'bypass', label: t('pages.settings.subHappPerAppBypass') },
]}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappPerAppList')}
description={t('pages.settings.subHappPerAppListDesc')}
>
<Input.TextArea
value={allSetting.subHappPerAppList}
rows={4}
placeholder="org.telegram.messenger, com.google.android.youtube"
onChange={(e) => updateSetting({ subHappPerAppList: e.target.value })}
/>
</SettingListItem>
</>
),
},
{
key: 'failover',
label: (
<span>
<CloudSyncOutlined /> {!isMobile && t('pages.settings.subHappGroupFailover')}
</span>
label: catTabLabel(
<CloudSyncOutlined />,
t('pages.settings.subHappGroupFailover'),
isMobile,
),
children: (
<>
@@ -538,47 +546,6 @@ export default function HappSettingsContent({
</>
),
},
{
key: 'android',
label: (
<span>
<MobileOutlined /> {!isMobile && t('pages.settings.subHappGroupAndroid')}
</span>
),
children: (
<>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappPerAppMode')}
description={t('pages.settings.subHappPerAppModeDesc')}
>
<Select
value={allSetting.subHappPerAppMode || 'off'}
style={{ width: '100%' }}
onChange={(v) => updateSetting({ subHappPerAppMode: v })}
options={[
{ value: 'off', label: t('pages.settings.subHappPerAppOff') },
{ value: 'on', label: t('pages.settings.subHappPerAppOn') },
{ value: 'bypass', label: t('pages.settings.subHappPerAppBypass') },
]}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappPerAppList')}
description={t('pages.settings.subHappPerAppListDesc')}
>
<Input.TextArea
value={allSetting.subHappPerAppList}
rows={4}
placeholder="org.telegram.messenger, com.google.android.youtube"
onChange={(e) => updateSetting({ subHappPerAppList: e.target.value })}
/>
</SettingListItem>
</>
),
},
]}
/>
@@ -414,8 +414,6 @@ export default function SubscriptionGeneralTab({
updateSetting={updateSetting}
isMobile={isMobile}
remoteSourceBadge={remoteSourceBadge}
// QR settings links select the link control; ordinary Happ visits still start on routing.
defaultActiveTab={searchParams.get('happTab') === 'links' ? 'links' : 'routing'}
/>
),
},
+1 -1
View File
@@ -241,7 +241,7 @@ describe('ClientQrModal Happ presentation', () => {
expect(view.onOpenChange).toHaveBeenCalledOnce();
expect(view.onOpenChange).toHaveBeenCalledWith(false);
expect(screen.getByTestId('location').textContent).toBe(
'/settings?subscriptionTab=happ&happTab=links#subscription',
'/settings?subscriptionTab=happ#subscription',
);
expect(HttpUtil.post).not.toHaveBeenCalled();
});
@@ -144,7 +144,7 @@ describe('SubscriptionGeneralTab', () => {
});
it.each([false, true])(
'updates the Happ link gate from its own tab when stored as %s',
'updates the Happ link gate beside auto-detection when stored as %s',
(enabled) => {
const updateSetting = vi.fn();
@@ -158,11 +158,6 @@ describe('SubscriptionGeneralTab', () => {
);
fireEvent.click(screen.getByRole('tab', { name: /Happ/ }));
expect(
screen.getByRole('tab', { name: /Routing & Rules/ }).getAttribute('aria-selected'),
).toBe('true');
expect(screen.queryByRole('switch', { name: 'Encrypted subscription links' })).toBeNull();
fireEvent.click(screen.getByRole('tab', { name: /Subscription Links/ }));
const linkSwitch = screen.getByRole('switch', { name: 'Encrypted subscription links' });
expect(linkSwitch.getAttribute('aria-checked')).toBe(String(enabled));
expect(updateSetting).not.toHaveBeenCalled();
@@ -172,51 +167,24 @@ describe('SubscriptionGeneralTab', () => {
},
);
it('opens the Happ link tab from the QR settings deep link without enabling generation', () => {
it('shows the Happ link gate from the QR settings deep link without enabling generation', () => {
const updateSetting = vi.fn();
renderWithProviders(
<MemoryRouter initialEntries={['/settings?subscriptionTab=happ&happTab=links#subscription']}>
<MemoryRouter initialEntries={['/settings?subscriptionTab=happ#subscription']}>
<SubscriptionGeneralTab
allSetting={new AllSetting({ happLinkEnable: false })}
updateSetting={updateSetting}
/>
<LocationProbe />
</MemoryRouter>,
);
expect(screen.getByRole('tab', { name: /Happ/ }).getAttribute('aria-selected')).toBe('true');
expect(
screen.getByRole('tab', { name: /Subscription Links/ }).getAttribute('aria-selected'),
).toBe('true');
expect(
screen
.getByRole('switch', { name: 'Encrypted subscription links' })
.getAttribute('aria-checked'),
).toBe('false');
expect(screen.getByTestId('location').textContent).toBe(
'/settings?subscriptionTab=happ&happTab=links#subscription',
);
expect(updateSetting).not.toHaveBeenCalled();
});
it.each(['', '&happTab=unknown'])(
'keeps the routing default for a general Happ deep link %s',
(query) => {
const updateSetting = vi.fn();
renderWithProviders(
<MemoryRouter initialEntries={['/settings?subscriptionTab=happ' + query + '#subscription']}>
<SubscriptionGeneralTab allSetting={new AllSetting()} updateSetting={updateSetting} />
</MemoryRouter>,
);
expect(screen.getByRole('tab', { name: /Happ/ }).getAttribute('aria-selected')).toBe('true');
expect(
screen.getByRole('tab', { name: /Routing & Rules/ }).getAttribute('aria-selected'),
).toBe('true');
expect(screen.queryByRole('switch', { name: 'Encrypted subscription links' })).toBeNull();
expect(updateSetting).not.toHaveBeenCalled();
},
);
});