Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ User-facing changes only. The release workflow publishes the matching

- A Setups page that opens first: every setup with its HUD's picture and key details, and
one click to switch, or to switch and play.
- An Edit page for the setup in the game: every setting on one page, grouped as Look, Feel,
Sound, Mods, Performance and Advanced, each showing its current value. Click a row to open the
page that changes it.

### Fixed

Expand All @@ -20,7 +23,10 @@ User-facing changes only. The release workflow publishes the matching

### Changed

- No more fold-out sections: every setting is on the page. Comfig's modules have their own tab
- Comfig is now called Performance, and the sidebar uses the Edit page's groups: Look (HUD,
Crosshair, Viewmodels), Feel (Gameplay, Binds), then Sounds, Mods and Performance, Advanced
(Files, Launch) and Inventory.
- No more fold-out sections: every setting is on the page. Performance's modules have their own tab
with search and category filters; HUD options, Mods' file check, Casual addons and particles,
and App settings' credits are always shown; Sounds puts pitch and repeat delay with each sound.
- Viewmodels shows each class as one table, with every weapon under its slot and its own Shown,
Expand All @@ -30,7 +36,7 @@ User-facing changes only. The release workflow publishes the matching
- Crosshair, Viewmodels and Mods' Casual setup show the same bar while changes aren't in TF2 yet,
with Discard changes and Apply changes. Viewmodels applies straight from that bar, without a
separate review window.
- Comfig shows an update button only when a newer mastercomfig is out, and keeps the comfig.app
- Performance shows an update button only when a newer mastercomfig is out, and keeps the comfig.app
customizer next to the comfig-custom folder it makes. Removing the crosshair pack is a button at
the bottom of the Crosshair page instead of a hidden menu, and buttons that repeated a switch or
the sidebar (Open Comfig addons, Open Viewmodels) are gone.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/App.boot.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ it("holds the startup screen over an inert app until the first pane has loaded",
await act(async () => root.render(<App api={api} preview="settings-comfig" bootSplash />));

const splash = box.querySelector('[data-testid="boot-splash"]');
expect(splash?.textContent).toContain("Loading Comfig…");
expect(splash?.textContent).toContain("Loading Performance…");
expect(splash?.nextElementSibling?.hasAttribute("inert")).toBe(true);

await act(async () => finishRead());
Expand Down
25 changes: 22 additions & 3 deletions apps/desktop/src/App.setups.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,26 @@ it("opens on Setups with the panes kept, hidden, behind it", async () => {
expect(text(".setups-name")).toBe("Main");
});

it("goes to the last pane from Edit and back with the Setups entry", async () => {
it("opens the Edit page from Edit, a pane from its rows, and back with the Setups entry", async () => {
await click('[data-testid="setups-edit"]');
expect(q('[data-testid="setups-home"]')).toBeNull();
expect(current("settings-tab-comfig")).toBe("page");
expect(q('[data-testid="settings-pane-comfig"] > [hidden]')).toBeNull();
expect(q('[data-testid="setup-sheet"]')).not.toBeNull();
expect(current("settings-tab-edit")).toBe("page");
expect(text('[data-testid="settings-tab-edit"]')).toBe("Edit Main");
expect(text(".setup-sheet-name")).toBe("Main");
expect(
Array.from(box.querySelectorAll(".setup-group-title")).map((title) => title.textContent),
).toEqual(["Look", "Feel", "Sound", "Mods", "Performance", "Advanced"]);

await click('[data-testid="setup-row-fov"]');
expect(q('[data-testid="setup-sheet"]')).toBeNull();
expect(current("settings-tab-gameplay")).toBe("page");
expect(q('[data-testid="settings-pane-gameplay"] > [hidden]')).toBeNull();

await click('[data-testid="settings-tab-edit"]');
expect(q('[data-testid="setup-sheet"]')).not.toBeNull();
await click('[data-testid="setup-row-launch"]');
expect(current("settings-tab-launch")).toBe("page");

await click('[data-testid="settings-tab-binds"]');
expect(current("settings-tab-binds")).toBe("page");
Expand All @@ -100,6 +115,10 @@ it("opens the guide's profiles page from Setups and the pane's page elsewhere",
await click('[data-testid="settings-tab-binds"]');
await click('[data-testid="guide-open"]');
expect(external).toHaveBeenLastCalledWith(expect.stringMatching(/\/docs\/guide\/binds\.md$/));
// The Edit page is described with the setups.
await click('[data-testid="settings-tab-edit"]');
await click('[data-testid="guide-open"]');
expect(external).toHaveBeenLastCalledWith(expect.stringMatching(/\/docs\/guide\/profiles\.md$/));
});

it("returns to Setups from App settings", async () => {
Expand Down
50 changes: 32 additions & 18 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
import { createSettingsDraftStore } from "./lib/settings-drafts";
import {
browserStorage,
editLabel,
readLastPane,
SETTINGS_TAB_LABELS,
type SettingsTab,
Expand Down Expand Up @@ -117,6 +118,8 @@ export function App({
const [appSettingsOpen, setAppSettingsOpen] = useState(false);
// The workspace opens on Setups; a fixture that picks a pane opens on that.
const [setupsOpen, setSetupsOpen] = useState(() => previewSettingsTab(preview) === null);
// The Edit page: every setting of the setup in the game, one row per setting.
const [sheetOpen, setSheetOpen] = useState(() => preview === "settings-edit");
const [launchSync, setLaunchSync] = useState<LaunchSyncStatus | null>(null);
const [launchSyncPrompt, setLaunchSyncPrompt] = useState<LaunchSyncStatus | null>(null);
const appSettingsButton = useRef<HTMLButtonElement>(null);
Expand All @@ -136,12 +139,19 @@ export function App({
const navigateSettings = useCallback((tab: SettingsTab) => {
setAppSettingsOpen(false);
setSetupsOpen(false);
setSheetOpen(false);
setSettingsTab(tab);
}, []);
const openSetups = useCallback(() => {
setAppSettingsOpen(false);
setSheetOpen(false);
setSetupsOpen(true);
}, []);
const openSheet = useCallback(() => {
setAppSettingsOpen(false);
setSetupsOpen(false);
setSheetOpen(true);
}, []);
const reviewSettings = useCallback(
(tab: SettingsTab) => {
navigateSettings(tab);
Expand Down Expand Up @@ -289,6 +299,15 @@ export function App({
if (filesVersion >= 0 && filesDraftStore.hasDirty(changedProfileId)) tabs.add("files");
return tabs;
}, [settingsDrafts, filesVersion, filesDraftStore, changedProfileId]);
const activeProfileName =
profiles.library?.profiles.find((profile) => profile.id === profiles.library?.activeProfileId)
?.name ?? null;
// What the workspace shows when App settings is closed.
const workspaceName = setupsOpen
? "Setups"
: sheetOpen
? editLabel(activeProfileName)
: SETTINGS_TAB_LABELS[settingsTab];
const pendingPanes = [
...new Set(settingsDrafts.map((entry) => SETTINGS_TAB_LABELS[entry.tab])),
].join(", ");
Expand Down Expand Up @@ -452,7 +471,7 @@ export function App({
surface,
settingsOpen,
settingsSettled,
paneLabel: setupsOpen ? "Setups" : SETTINGS_TAB_LABELS[settingsTab],
paneLabel: sheetOpen ? (activeProfileName ?? "Edit") : workspaceName,
failed: error !== null,
};
const boot = bootStage(stageInput);
Expand Down Expand Up @@ -585,9 +604,7 @@ export function App({
backAction={
<button type="button" className="btn btn-ghost" onClick={closeAppSettings}>
<ArrowLeft size={15} aria-hidden="true" />
{settingsOpen
? `Back to ${setupsOpen ? "Setups" : SETTINGS_TAB_LABELS[settingsTab]}`
: "Back to setup"}
{settingsOpen ? `Back to ${workspaceName}` : "Back to setup"}
</button>
}
onChangeInstall={() =>
Expand Down Expand Up @@ -766,7 +783,9 @@ export function App({
<SettingsLayout
tab={settingsTab}
changed={changedTabs}
page={appSettingsOpen ? "app" : setupsOpen ? "setups" : null}
page={appSettingsOpen ? "app" : setupsOpen ? "setups" : sheetOpen ? "edit" : null}
editLabel={editLabel(activeProfileName)}
onEdit={openSheet}
onTab={navigateSettings}
onSetups={openSetups}
scrollIdentity={`${path}:${profiles.library?.activeProfileId ?? "none"}`}
Expand All @@ -776,13 +795,13 @@ export function App({
type="button"
data-testid="guide-open"
className="settings-nav-item"
title={`Guide for ${appSettingsOpen ? "App settings" : setupsOpen ? "Setups" : SETTINGS_TAB_LABELS[settingsTab]}`}
title={`Guide for ${appSettingsOpen ? "App settings" : workspaceName}`}
onClick={() =>
void openExternal(
guideUrl(
appSettingsOpen
? "app-settings"
: setupsOpen
: setupsOpen || sheetOpen
? "profiles"
: settingsTab,
update.version,
Expand Down Expand Up @@ -813,17 +832,16 @@ export function App({
<SettingsHost
api={api}
visible={!appSettingsOpen && !setupsOpen}
sheet={sheetOpen}
changed={changedTabs}
onBack={openSetups}
filesDraftStore={filesDraftStore}
filesSaver={filesExit.saver}
filesCloseReady={filesExit.ready}
settingsDraftStore={settingsDraftStore}
tab={settingsTab}
activeProfileId={profiles.library?.activeProfileId ?? null}
activeProfileName={
profiles.library?.profiles.find(
(profile) => profile.id === profiles.library?.activeProfileId,
)?.name ?? null
}
activeProfileName={activeProfileName}
onNavigate={navigateSettings}
onTryComfig={tryComfig}
running={lock.running}
Expand Down Expand Up @@ -859,7 +877,7 @@ export function App({
onSwitch={requestSwitch}
onSwitchAndPlay={switchAndPlay}
onPlay={launchNow}
onEdit={() => navigateSettings(settingsTab)}
onEdit={openSheet}
onCreateNew={() => filesExit.request(firstRun.openCreate)}
onCompare={openCompare}
/>
Expand Down Expand Up @@ -889,11 +907,7 @@ export function App({
<ToastProvider>
<WelcomeTour
open={welcome !== null && settingsOpen && !progress.state.active}
profileName={
profiles.library?.profiles.find(
(profile) => profile.id === profiles.library?.activeProfileId,
)?.name ?? null
}
profileName={activeProfileName}
origin={welcome ?? "saved"}
offerComfig={welcomeOffersComfig}
onClose={() => setWelcome(null)}
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/ComfigPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export function ComfigPane({
if (detail && detail.layer !== "comfig") {
return (
<section data-testid="settings-comfig" className="min-w-0 text-left">
<PaneHeader title="Comfig" />
<PaneHeader title="Performance" />
<div data-testid="comfig-vanilla-gate" className="comfig-empty">
<h2 className="t-section">This profile doesn't use mastercomfig</h2>
{onTryComfig ? (
Expand All @@ -249,7 +249,7 @@ export function ComfigPane({
<section data-testid="settings-comfig" className="min-w-0 text-left">
<PaneHeader
compact
title="Comfig"
title="Performance"
actions={
statusProblem ? (
<p aria-live="polite" className="badge">
Expand Down Expand Up @@ -277,7 +277,7 @@ export function ComfigPane({
},
]}
selected={view}
label="Comfig"
label="Performance"
idPrefix="comfig-view"
panelId="comfig-view-panel"
onSelect={setView}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/FilesPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1351,7 +1351,7 @@ function owningPane(path: string): { tab: SettingsTab; label: string } {
if (path.includes("execs_binds")) return { tab: "binds", label: "Open Binds" };
if (path.includes("execs_gameplay")) return { tab: "gameplay", label: "Open Gameplay" };
if (path.includes("execs_preload")) return { tab: "mods", label: "Open Mods" };
return { tab: "comfig", label: "Open Comfig" };
return { tab: "comfig", label: "Open Performance" };
}

function FindingRow({
Expand Down
49 changes: 43 additions & 6 deletions apps/desktop/src/SettingsHost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import {
} from "./lib/files-drafts";
import { cfgHudFolder } from "./lib/files-reference";
import { blockingFindingsForFile, cfgFileMeta, hitAnalysisLimit } from "./lib/files-ui";
import { gameplayPath } from "./lib/gameplay-ui";
import { gameplayPath, seedGameplay } from "./lib/gameplay-ui";
import { hudOverlayCrosshairState } from "./lib/hud-ui";
import { recommendedLaunchOptions } from "./lib/launch-ui";
import { type ModInstallResult, type ModSelection, PRELOADER_REPO_URL } from "./lib/mods-ui";
Expand All @@ -66,6 +66,7 @@ import { SETTINGS_TAB_LABELS, type SettingsTab } from "./lib/settings-ui";
import type { IncomingSounds } from "./lib/sound-library";
import { prefetchViewmodelCatalog } from "./lib/viewmodel-catalog-cache";
import { ModsPane } from "./ModsPane";
import { SetupSheet } from "./SetupSheet";
import { SoundsPane } from "./SoundsPane";
import { ViewmodelPane } from "./ViewmodelPane";

Expand All @@ -82,7 +83,10 @@ export function SettingsHost({
tab,
activeProfileId,
activeProfileName,
visible = true,
visible: hostVisible = true,
sheet = false,
changed,
onBack,
running,
externalBusy,
refreshKey,
Expand Down Expand Up @@ -112,6 +116,12 @@ export function SettingsHost({
activeProfileName?: string | null;
/** Global pages retain drafts but release auditions, key capture and reads. */
visible?: boolean;
/** Show the Edit page instead of the pane; the panes keep their drafts. */
sheet?: boolean;
/** Panes with changes that have not reached the profile yet. */
changed?: ReadonlySet<SettingsTab>;
/** Leaves the Edit page for the Setups page. */
onBack?: () => void;
running: boolean;
externalBusy: boolean;
refreshKey: string | number;
Expand All @@ -136,6 +146,9 @@ export function SettingsHost({
onLaunchOptionsSaved?: () => void;
}) {
const { error, dismissError } = useAppStatus();
// The Edit page shows no pane: panes release their reads as on a global page.
const visible = hostVisible && !sheet;
const sheetShown = hostVisible && sheet;
const toast = useToast();
const [queueBusy, setQueueBusy] = useState(false);
const [loadedDetail, setDetail] = useState<ProfileDetail | null>(null);
Expand Down Expand Up @@ -288,6 +301,8 @@ export function SettingsHost({
() => mapsFromFiles(files, layer, detail?.files, detail ?? undefined),
[files, layer, detail],
);
// Values on the Edit page come only from a complete read of this profile.
const cfgReadable = maps.complete && !filesLimited && cfgSnapshotProfileId === profileId;
// Only the visible pane pays for the extra override probe.
const provenanceTab = visible && !filesLimited ? tab : null;
const provenance = useMemo(
Expand Down Expand Up @@ -1672,10 +1687,10 @@ export function SettingsHost({
</button>
</div>
) : null}
{filesLimited && (usesCfgState(tab) || tab === "files") ? (
{filesLimited && (sheetShown || usesCfgState(tab) || tab === "files") ? (
<div role="alert" className="mb-4 text-warn">
<p>{cfgReadProblem}</p>
{usesCfgState(tab) && cfgSnapshotProfileId === profileId ? (
{(sheetShown || usesCfgState(tab)) && cfgSnapshotProfileId === profileId ? (
<p>Values below are from the last complete read and may be stale.</p>
) : null}
<div className="pane-actions mt-2">
Expand All @@ -1687,7 +1702,7 @@ export function SettingsHost({
>
Retry loading settings
</button>
{tab !== "files" && onNavigate ? (
{(sheetShown || tab !== "files") && onNavigate ? (
<button type="button" className="btn btn-ghost" onClick={() => onNavigate("files")}>
Review in Files
</button>
Expand Down Expand Up @@ -1716,7 +1731,7 @@ export function SettingsHost({
{!profileId && loading && !identityPending ? (
<LoadingState>Loading settings…</LoadingState>
) : null}
{!identityPending && !filesLimited && !maps.complete && usesCfgState(tab) ? (
{!identityPending && !filesLimited && !maps.complete && (sheetShown || usesCfgState(tab)) ? (
<div role="alert" className="mb-4 text-warn">
<p>{maps.reason ?? CFG_INCOMPLETE_MESSAGE}</p>
{onNavigate && (
Expand All @@ -1735,6 +1750,28 @@ export function SettingsHost({
)}
</div>
) : null}
{sheetShown && detail && !identityPending ? (
<SetupSheet
api={api}
input={{
detail,
gameplay: cfgReadable
? seedGameplay(
files.find((file) => file.path === gameplayPath(layer))?.text ?? "",
maps.effective,
)
: null,
boundKeys: cfgReadable ? Object.keys(maps.binds).length : null,
comfig,
launchOptions: launchSeed,
cfgFiles: files.length,
}}
refreshKey={`${refreshKey}:${profileId}`}
changed={changed}
onOpen={(next) => onNavigate?.(next)}
onBack={() => onBack?.()}
/>
) : null}
<div hidden={!visible || tab !== "inventory"}>
<AutosaveActivity.Provider value={visible && tab === "inventory"}>
<InventoryPane
Expand Down
Loading
Loading