feat(shell): draw only the destinations the user kept
The navigation reads the resolved map from the backend rather than holding a copy of the defaults, which would be the copy that shipped in the binary rather than the one being edited. Hiding takes away the nav item and nothing else: `navigate` still resolves a hidden view, which detail views and the launch page depend on. No special case was needed for the highlight, because #72 moved that onto `active-view-store` -- the sidebar asks `isActive(id)` per *rendered* item, so a hidden view lights nothing exactly as a detail view does. Downloads is gated at the nav on `downloadStore.available` rather than in the config, so switching it on in Settings still means what it says once a client exists, and the tab appears without a restart. `available` is false until the providers have loaded, which makes the item appear on a fresh launch rather than appearing and then vanishing. The tab bar honours the toggles too, and the reason is local rather than a general rule about phones: "More" opens the *same* `<app-sidebar>`, which filters, so an unfiltered bar would contradict its own drawer one tap away. Which four tabs is still plan 016's subset; this only removes from it, and "More" is never filtered. `services/view-meta.ts` is the destination list, on `shortcut-meta.ts`'s pattern, because Settings is now a second reader of the same labels in the same order. Two existing sidebar tests had to say which world they describe: eleven destinations now assumes a configured download client.
This commit is contained in:
@@ -27,6 +27,9 @@ import type * as library from '@go/library/models.js';
|
||||
import { ThemeController } from '@store/controllers/theme-controller';
|
||||
import { TrackListController } from '@store/controllers/tracklist-controller';
|
||||
import { FavoritesController } from '@store/controllers/favorites-controller';
|
||||
import { ViewVisibilityController } from '@store/controllers/view-visibility-controller';
|
||||
import { VIEW_META } from '../../services/view-meta';
|
||||
import { downloadStore } from '@store/download-store';
|
||||
import { GetAllPlaylists } from '@go/playlist/service.js';
|
||||
import type * as playlist from '@go/playlist/models.js';
|
||||
import { Events } from '../../events';
|
||||
@@ -71,6 +74,9 @@ export class ConfigPage extends ViewLifecycleMixin(LitElement) {
|
||||
// --- Favorites controller ---
|
||||
private favCtrl = new FavoritesController(this);
|
||||
|
||||
/** Which destinations the navigation offers (#25). */
|
||||
private viewsCtrl = new ViewVisibilityController(this);
|
||||
|
||||
// --- Shortcuts controller ---
|
||||
private shortcutsCtrl = new ShortcutsController(this);
|
||||
|
||||
@@ -469,6 +475,12 @@ export class ConfigPage extends ViewLifecycleMixin(LitElement) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.view-note {
|
||||
color: var(--yj-text-tertiary, #888);
|
||||
font-size: var(--yj-font-size-sm, 0.85rem);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.column-arrows {
|
||||
display: flex;
|
||||
gap: 0.15em;
|
||||
@@ -1084,6 +1096,25 @@ export class ConfigPage extends ViewLifecycleMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private handleViewToggle = (
|
||||
view: string,
|
||||
visible: boolean,
|
||||
): void => {
|
||||
this.viewsCtrl
|
||||
.setVisible(view, visible)
|
||||
.catch((err: unknown) => {
|
||||
console.error('Failed to save view visibility:', err);
|
||||
notificationStore.transient({
|
||||
key: 'view-visibility',
|
||||
text: `Could not change which views are shown. ${describeError(err)}`,
|
||||
detail: String(err),
|
||||
});
|
||||
// The checkbox has already flipped itself; the store is
|
||||
// the truth, so redraw from it.
|
||||
this.requestUpdate();
|
||||
});
|
||||
};
|
||||
|
||||
private handleDefaultPageChange = (
|
||||
e: CustomEvent<ConfigFieldChangeEvent>,
|
||||
): void => {
|
||||
@@ -1428,6 +1459,7 @@ export class ConfigPage extends ViewLifecycleMixin(LitElement) {
|
||||
-->
|
||||
${this.renderLibrarySection()}
|
||||
${this.renderGeneralSection()}
|
||||
${this.renderNavigationSection()}
|
||||
${this.renderNowPlayingSection()}
|
||||
${this.renderThemeSection()}
|
||||
${this.renderTrackListSection()}
|
||||
@@ -1678,6 +1710,79 @@ export class ConfigPage extends ViewLifecycleMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
// --- Navigation section ---
|
||||
|
||||
/**
|
||||
* Which destinations the sidebar and the phone's tab bar offer.
|
||||
*
|
||||
* Two items are drawn but not editable, and both say why in place
|
||||
* rather than being silently inert. Settings is never hideable --
|
||||
* the backend refuses it too, because `config.toml` is
|
||||
* hand-editable. The launch page is not hideable *while it is the
|
||||
* launch page*, which is a state the user can leave by changing the
|
||||
* launch page above; refusing is preferable to the alternatives,
|
||||
* since resetting their launch page silently changes a second thing
|
||||
* they chose and allowing it lands the app on a page nothing points
|
||||
* at.
|
||||
*/
|
||||
private renderNavigationSection() {
|
||||
return html`
|
||||
<config-section
|
||||
heading="Navigation"
|
||||
description="Choose which destinations the sidebar and the phone's tab bar offer. Hiding one does not remove it — links and the launch page still open it."
|
||||
>
|
||||
<ul class="column-list">
|
||||
${repeat(VIEW_META, (v) => v.id, (v) => {
|
||||
const checked = this.viewsCtrl.enabled(v.id);
|
||||
const isLaunchPage = this.defaultPage === v.id;
|
||||
const locked = v.alwaysShown === true || isLaunchPage;
|
||||
|
||||
let note = '';
|
||||
|
||||
if (v.alwaysShown === true) {
|
||||
note = 'Always shown.';
|
||||
} else if (isLaunchPage) {
|
||||
note = 'This is the launch page.';
|
||||
} else if (
|
||||
v.id === 'downloads' &&
|
||||
checked &&
|
||||
!downloadStore.available
|
||||
) {
|
||||
// The config says show it and the nav does not, which
|
||||
// would otherwise read as the checkbox not working.
|
||||
note = 'Hidden until a download client is configured.';
|
||||
}
|
||||
|
||||
return html`
|
||||
<li
|
||||
class="column-item ${checked ? 'enabled' : 'disabled'}"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="column-toggle"
|
||||
aria-label="Show ${v.label} in the navigation"
|
||||
.checked=${checked}
|
||||
?disabled=${locked}
|
||||
@change=${(e: Event) =>
|
||||
this.handleViewToggle(
|
||||
v.id,
|
||||
(e.target as HTMLInputElement).checked,
|
||||
)}
|
||||
/>
|
||||
<span class="column-label">
|
||||
${v.label}
|
||||
</span>
|
||||
${note
|
||||
? html`<span class="view-note">${note}</span>`
|
||||
: nothing}
|
||||
</li>
|
||||
`;
|
||||
})}
|
||||
</ul>
|
||||
</config-section>
|
||||
`;
|
||||
}
|
||||
|
||||
// --- Theme section ---
|
||||
|
||||
private renderThemeSection() {
|
||||
|
||||
Reference in New Issue
Block a user