diff --git a/web/src/App.svelte b/web/src/App.svelte index f28fdd2..4bd8ec8 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -27,6 +27,7 @@ edgeSlotWriteTooltip as edgeOnboardSlotWriteTooltip, emptyEdgeOnboardProfileState, friendlyEdgeSlotsError, + isEdgeSlotsAgentRequiredMessage, isEdgeTargetController, shouldReadEdgeOnboardProfiles, shouldResetEdgeOnboardProfiles @@ -787,7 +788,8 @@ let requestedView: AppView | null = null; const guardBounceMessages: Partial> = { tuning: 'Tuning opens once a controller is connected.', - advancedButtonMapping: 'Button mapping needs a game selected in Tuning first.' + advancedButtonMapping: 'Button mapping needs a game selected in Tuning first.', + advancedEdgeSlots: 'Edge onboard slots need a DualSense Edge as the Target Controller.' }; const syncViewFromHash = () => { @@ -801,7 +803,12 @@ }; const navigateToView = (view: AppView) => { - view = guardView(view, { tuningReady, buttonMappingReady, edgeSlotsReady }); + const requested = view; + view = guardView(requested, { tuningReady, buttonMappingReady, edgeSlotsReady }); + if (view !== requested) { + const message = guardBounceMessages[requested]; + if (message) showToast(message, 'info'); + } activeView = view; // An explicit navigation always wins over a parked deep-link intent. requestedView = null; @@ -1541,7 +1548,7 @@ await loadEdgeProfiles(controller.id, true); } catch (caught) { edgeProfilesError = friendlyEdgeSlotsError(caught, 'Unable to write Edge onboard slot.'); - showToast(edgeProfilesError, 'error'); + showToast(edgeProfilesError, isEdgeSlotsAgentRequiredMessage(edgeProfilesError) ? 'info' : 'error'); } finally { edgeProfilesBusySlot = ''; } @@ -2288,9 +2295,9 @@ {:else if snapshot} - +
@@ -2501,6 +2501,7 @@ {edgeProfilesLoading} {edgeProfilesBusySlot} {edgeProfilesError} + edgeProfilesAgentRequired={isEdgeSlotsAgentRequiredMessage(edgeProfilesError)} {edgeSlotsReadTooltip} edgeSlotWriteLabel={edgeSlotWriteLabel()} onRefreshEdgeProfiles={() => controller && void loadEdgeProfiles(controller.id, true)} diff --git a/web/src/app/edgeOnboardProfiles.ts b/web/src/app/edgeOnboardProfiles.ts index a931a51..f781981 100644 --- a/web/src/app/edgeOnboardProfiles.ts +++ b/web/src/app/edgeOnboardProfiles.ts @@ -97,10 +97,15 @@ export function edgeSlotWriteLabel(edgeProfiles: EdgeProfilesResponse | null): s * helper handles both the read and the write path, so the copy can't claim a * direction. */ +export const EDGE_SLOTS_AGENT_REQUIRED_MESSAGE = 'Onboard slots need DSCC running.'; + +export const isEdgeSlotsAgentRequiredMessage = (message: string): boolean => + message === EDGE_SLOTS_AGENT_REQUIRED_MESSAGE; + export const friendlyEdgeSlotsError = (caught: unknown, fallback: string): string => { const message = caught instanceof Error ? caught.message : fallback; return message.includes('requires the real DSCC agent') - ? 'Onboard slots need DSCC running.' + ? EDGE_SLOTS_AGENT_REQUIRED_MESSAGE : message; }; diff --git a/web/src/components/AppSidebar.svelte b/web/src/components/AppSidebar.svelte index e6c4284..9747af0 100644 --- a/web/src/components/AppSidebar.svelte +++ b/web/src/components/AppSidebar.svelte @@ -32,7 +32,7 @@ const itemTooltip = (id: AppView): string => { if (id === 'tuning' && !readiness.tuningReady) return 'Select a controller before tuning haptics.'; if (id === 'advancedButtonMapping' && !readiness.buttonMappingReady) { - return 'Select a game or local app scope before editing mappings.'; + return 'Pick a supported game in Tuning to map its buttons.'; } if (id === 'advancedEdgeSlots' && !readiness.edgeSlotsReady) { return 'Onboard slots are available when the Target Controller is a DualSense Edge.'; @@ -50,9 +50,10 @@ @@ -68,9 +69,10 @@ diff --git a/web/src/lib/features/controllers/ControllersView.svelte b/web/src/lib/features/controllers/ControllersView.svelte index a272a32..e6bbfc8 100644 --- a/web/src/lib/features/controllers/ControllersView.svelte +++ b/web/src/lib/features/controllers/ControllersView.svelte @@ -63,6 +63,10 @@ export let onSetStickDeadzone: (side: 'left' | 'right', value: number) => void | Promise = () => {}; export let onStartInputBridge: () => void | Promise = () => {}; export let onStopInputBridge: () => void | Promise = () => {}; + export let glyphOverrideEnabled = false; + export let glyphOverrideBusy = false; + export let glyphOverrideTitle = ''; + export let onToggleGlyphOverride: () => void | Promise = () => {}; export let supportBundleBusy: 'copy' | 'download' | '' = ''; export let onDownloadSupportBundle: () => void | Promise = () => {}; @@ -614,6 +618,25 @@
{/if} +
+
Button icons
+
+
+ Forza button icons + Show PlayStation icons where DSCC manages the supported Forza icon files. +
+ +
+
+
Support
diff --git a/web/src/lib/features/controllers/EdgeSlotsView.svelte b/web/src/lib/features/controllers/EdgeSlotsView.svelte index 0026f82..e3aa2e4 100644 --- a/web/src/lib/features/controllers/EdgeSlotsView.svelte +++ b/web/src/lib/features/controllers/EdgeSlotsView.svelte @@ -13,6 +13,7 @@ export let edgeProfilesLoading = false; export let edgeProfilesBusySlot = ''; export let edgeProfilesError = ''; + export let edgeProfilesAgentRequired = false; export let edgeSlotsReadTooltip = ''; export let edgeSlotWriteLabel = 'Write'; export let onRefreshEdgeProfiles: () => void | Promise = () => {}; @@ -25,7 +26,7 @@ $: alias = controller?.name || controller?.family || 'No controller'; -
+

Edge onboard slots

{alias} · profiles stored on the controller itself, for checking, not for everyday tuning @@ -49,7 +50,17 @@
{#if edgeProfilesError} -

{edgeProfilesError}

+ {#if edgeProfilesAgentRequired} +

+ Info + {edgeProfilesError} Start DSCC to read onboard slots. +

+ {:else} + + {/if} {:else if edgeProfiles?.warning}

{edgeProfiles.warning}

{/if} diff --git a/web/src/lib/features/tuning/TuningHeader.svelte b/web/src/lib/features/tuning/TuningHeader.svelte index 7b43f55..94ad6a2 100644 --- a/web/src/lib/features/tuning/TuningHeader.svelte +++ b/web/src/lib/features/tuning/TuningHeader.svelte @@ -323,9 +323,10 @@ aria-haspopup="menu" aria-expanded={profileMenuOpen} disabled={!profiles.length} + title="Choose the saved profile to edit in this tuning scope." onclick={toggleProfileMenu} > - Profile: + Saved profile: {profileName} diff --git a/web/src/styles/controllers.css b/web/src/styles/controllers.css index 07704f4..4104f4e 100644 --- a/web/src/styles/controllers.css +++ b/web/src/styles/controllers.css @@ -318,16 +318,61 @@ color: var(--ink-muted); } +.ctl-note.status { + display: flex; + align-items: flex-start; + gap: 6px; +} + +.ctl-note-label { + flex: none; + font-weight: 700; + color: var(--ink); +} + .ctl-note.error { color: var(--danger); } +.ctl-note.error .ctl-note-label { + color: var(--danger); +} + .ctl-suggestions { display: grid; gap: 6px; margin-top: 8px; } +.ctl-setting-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.ctl-setting-row > div { + display: grid; + gap: 2px; + min-width: 0; +} + +.ctl-setting-row strong { + font-size: 12px; + font-weight: 600; + color: var(--ink); +} + +.ctl-setting-row span { + font-size: 11px; + line-height: 1.45; + color: var(--ink-muted); +} + +.ctl-setting-row .ctl-button { + flex: none; +} + /* Support group */ .ctl-support { display: grid; @@ -337,6 +382,25 @@ } /* Edge onboard slots */ +.edge-slots-view { + max-width: 980px; + margin-inline: auto; +} + +@media (min-width: 1800px) { + .edge-slots-view { + padding-top: 44px; + } + + .edge-slots-view .ctl-groups { + max-width: 980px; + } + + .edge-slots-view .ctl-group { + max-width: 560px; + } +} + .edge-slot-list { display: grid; } diff --git a/web/src/styles/shell-v2.css b/web/src/styles/shell-v2.css index f41a4f4..0b0327b 100644 --- a/web/src/styles/shell-v2.css +++ b/web/src/styles/shell-v2.css @@ -76,6 +76,14 @@ html { background: color-mix(in srgb, var(--surface-raised) 60%, transparent); } +.sidebar-item.disabled, +.sidebar-item.disabled:hover { + color: var(--ink-muted); + background: none; + cursor: not-allowed; + opacity: 0.48; +} + .sidebar-item.active { color: var(--ink); background: var(--surface-raised); @@ -125,7 +133,7 @@ html { } /* Utility row above the views — cross-view context (target controller, web UI - bind address, glyph override, system readout). */ + bind address, system readout). */ .app-toolbar { display: flex; flex-direction: column; diff --git a/web/src/styles/status.css b/web/src/styles/status.css index 8d8defa..8ded511 100644 --- a/web/src/styles/status.css +++ b/web/src/styles/status.css @@ -259,3 +259,19 @@ min-height: calc(100dvh - 130px); /* viewport minus measured app chrome */ } } + +/* Very large windows: keep the user's wide/tall centering choice through the + 1200-1600px band, then cap the dead-band so Status stays near the toolbar. */ +@media (min-width: 1800px) and (min-height: 900px) { + .status-view { + justify-content: flex-start; + min-height: auto; + max-width: 1180px; + margin-inline: auto; + padding-top: 56px; + } + + .status-groups { + max-width: 1180px; + } +}