From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Modrinth Enhanced Date: Fri, 18 Sep 2026 09:43:57 +0200 Subject: [PATCH] Arrange the left sidebar The buttons above the instances were in a fixed order, and hiding one meant finding its switch among the feature settings, where three of them sat next to things that have nothing to do with the sidebar. Settings has a Sidebar tab now: every button in a list, moved up or down and shown or hidden, with a way back to the usual order. Right-clicking a button in the sidebar hides it, or opens that tab. The order and what is hidden are kept on this machine, not sent to Modrinth with the synced preferences: it is how the app looks here. The three switches move out of the feature settings, since the tab is where they belong; only the number of instances stays there. --- apps/app-frontend/src/App.vue | 138 ++++++++++++------ .../components/ui/modal/AppSettingsModal.vue | 22 ++- .../ui/settings/display/FeaturesSettings.vue | 70 --------- .../ui/settings/display/SidebarSettings.vue | 125 ++++++++++++++++ .../src/composables/use-sidebar-layout.ts | 72 +++++++++ 5 files changed, 310 insertions(+), 117 deletions(-) create mode 100644 apps/app-frontend/src/components/ui/settings/display/SidebarSettings.vue create mode 100644 apps/app-frontend/src/composables/use-sidebar-layout.ts diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 9679884..cb688fc 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -15,6 +15,7 @@ import { ChevronLeftIcon, ChevronRightIcon, CompassIcon, + EyeOffIcon, ImageIcon, LogInIcon, LogOutIcon, @@ -42,6 +43,7 @@ import { commonSettingsMessages, ContentInstallModal, ContentUpdaterModal, + ContextMenu, CreationFlowModal, defineMessages, I18nDebugPanel, @@ -79,7 +81,6 @@ import { computed, nextTick, onMounted, onUnmounted, provide, ref, watch } from import { RouterView, useRoute, useRouter } from 'vue-router' import AccountsCard from '@/components/ui/AccountsCard.vue' -import TitleBarAccountSwitcher from '@/components/ui/TitleBarAccountSwitcher.vue' import AppActionBar from '@/components/ui/AppActionBar.vue' import Breadcrumbs from '@/components/ui/Breadcrumbs.vue' import ErrorModal from '@/components/ui/ErrorModal.vue' @@ -107,6 +108,7 @@ import { markSyncInstancesUpdateNotificationShown, shouldShowSyncInstancesUpdateNotification, } from '@/components/ui/sync-instances-update-modal/show-notification' +import TitleBarAccountSwitcher from '@/components/ui/TitleBarAccountSwitcher.vue' import WindowControls from '@/components/ui/WindowControls.vue' import { useCheckDisableMouseover } from '@/composables/macCssFix.js' import { useAppEvent } from '@/composables/use-app-event' @@ -114,6 +116,7 @@ import { useAppSettings } from '@/composables/use-app-settings.ts' import { useError } from '@/composables/use-error.js' import { useInstanceMetadataRefresh } from '@/composables/use-instance-metadata-refresh' import { useQuickInstanceLimit } from '@/composables/use-quick-instance-limit.ts' +import { useSidebarLayout } from '@/composables/use-sidebar-layout' import { isDarkTheme, useTheme } from '@/composables/use-theme.ts' import { config } from '@/config' import { getAccountAppearance, rememberAccountAppearance } from '@/helpers/account-appearance.ts' @@ -259,6 +262,73 @@ const PRIDE_FUNDRAISER_END_DATE = new Date('2026-07-01T00:00:00Z').getTime() const credentials = ref() const storedModrinthAccounts = ref([]) let credentialsRefreshId = 0 +// The buttons above the instances. Which of them are shown, and in what order, +// is up to the player: Settings > Sidebar, or a right-click on one of them. +const sidebarLayout = useSidebarLayout() +const sidebarMenu = ref() + +const sidebarButtons = computed(() => { + const buttons = { + home: { + label: () => formatMessage(messages.home), + icon: PlayIcon, + iconClass: 'ml-0.5', + to: '/', + isPrimary: (r) => r.path === '/', + isSubpage: () => + (route.path.startsWith('/browse') || route.path.startsWith('/project')) && !!route.query.i, + }, + discover: { + label: () => formatMessage(commonMessages.discoverContentLabel), + icon: CompassIcon, + to: '/browse/modpack', + isPrimary: () => route.path.startsWith('/browse') && !route.query.i && !route.query.sid, + isSubpage: (r) => r.path.startsWith('/project') && !r.query.i && !r.query.sid, + }, + skins: { + label: () => formatMessage(appMessages.skinSelectorLabel), + icon: ShirtIcon, + to: '/skins', + }, + screenshots: { + label: () => formatMessage(messages.screenshots), + icon: ImageIcon, + to: '/screenshots', + }, + hosting: { + label: () => formatMessage(messages.modrinthHosting), + icon: ServerStackIcon, + to: '/hosting/manage', + isPrimary: (r) => r.path === '/hosting/manage' || r.path === '/hosting/manage/', + isSubpage: (r) => + (r.path.startsWith('/hosting/manage/') && r.path !== '/hosting/manage/') || + ((r.path.startsWith('/browse') || r.path.startsWith('/project')) && !!r.query.sid), + }, + } + + return sidebarLayout.order.value + .filter((id) => !sidebarLayout.isHidden(id)) + .map((id) => ({ id, ...buttons[id] })) +}) + +function openSidebarMenu(event, button) { + sidebarMenu.value?.open(event, [ + { + id: 'hide', + label: formatMessage(messages.hideFromSidebar, { name: button.label() }), + icon: EyeOffIcon, + action: () => sidebarLayout.setHidden(button.id, true), + }, + { type: 'divider' }, + { + id: 'arrange', + label: formatMessage(messages.arrangeSidebar), + icon: SettingsIcon, + action: () => appSettingsModal.value?.showSidebar(), + }, + ]) +} + const sidebarToggled = ref(true) // Whether the right sidebar is folded away, remembered across restarts in the // feature flags. The title bar button below is the only thing that sets it: @@ -729,6 +799,18 @@ const messages = defineMessages({ id: 'app.nav.screenshots', defaultMessage: 'Screenshots', }, + sidebarActions: { + id: 'app.nav.sidebar-actions', + defaultMessage: 'Sidebar button', + }, + hideFromSidebar: { + id: 'app.nav.hide-from-sidebar', + defaultMessage: 'Hide “{name}”', + }, + arrangeSidebar: { + id: 'app.nav.arrange-sidebar', + defaultMessage: 'Arrange the sidebar…', + }, createNewInstance: { id: 'app.nav.create-new-instance', defaultMessage: 'Create new instance', @@ -2346,52 +2428,15 @@ provideAppUpdateDownloadProgress(appUpdateDownload) class="app-grid-navbar bg-bg-raised flex flex-col p-[0.5rem] pt-0 gap-[0.25rem] w-[--left-bar-width]" > - - - - - - - - - - - - - + @@ -2475,6 +2520,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload) +
diff --git a/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue b/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue index 32a86a3..e409510 100644 --- a/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue +++ b/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue @@ -3,6 +3,7 @@ import { CoffeeIcon, HeartHandshakeIcon, LanguagesIcon, + LayoutTemplateIcon, LightBulbIcon, MicrochipIcon, ModrinthIcon, @@ -36,6 +37,7 @@ import AppearanceSettings from '@/components/ui/settings/display/AppearanceSetti import BehaviorSettings from '@/components/ui/settings/display/BehaviorSettings.vue' import FeatureFlagSettings from '@/components/ui/settings/display/FeatureFlagSettings.vue' import FeaturesSettings from '@/components/ui/settings/display/FeaturesSettings.vue' +import SidebarSettings from '@/components/ui/settings/display/SidebarSettings.vue' import LanguageSettings from '@/components/ui/settings/display/LanguageSettings.vue' import InstancesSyncedSettings from '@/components/ui/settings/instances/instances-synced-settings/index.vue' import JavaSettings from '@/components/ui/settings/instances/JavaSettings.vue' @@ -96,6 +98,15 @@ const tabs = [ icon: LightBulbIcon, content: FeaturesSettings, }, + { + name: defineMessage({ + id: 'app.settings.tabs.sidebar', + defaultMessage: 'Sidebar', + }), + category: tabCategories.display, + icon: LayoutTemplateIcon, + content: SidebarSettings, + }, { name: defineMessage({ id: 'app.settings.tabs.behavior', @@ -247,6 +258,15 @@ function showFeatureFlags(): void { modal.value?.show() } +/** Opens the settings at the sidebar tab, for the menu on a sidebar button. */ +function showSidebar(): void { + show() + const sidebarTabIndex = availableTabs.value.findIndex((tab) => tab.content === SidebarSettings) + if (sidebarTabIndex >= 0) { + modal.value?.setTab(sidebarTabIndex) + } +} + function showSyncedOptions(): void { const syncedOptionsTabIndex = availableTabs.value.findIndex( (tab) => tab.content === InstancesSyncedSettings, @@ -257,7 +277,7 @@ function showSyncedOptions(): void { modal.value?.show() } -defineExpose({ show, showProfile, showFeatureFlags, showSyncedOptions }) +defineExpose({ show, showProfile, showFeatureFlags, showSidebar, showSyncedOptions }) const { progress, version: downloadingVersion } = injectAppUpdateDownloadProgress() diff --git a/apps/app-frontend/src/components/ui/settings/display/FeaturesSettings.vue b/apps/app-frontend/src/components/ui/settings/display/FeaturesSettings.vue index 4e55cdd..9bc7ce3 100644 --- a/apps/app-frontend/src/components/ui/settings/display/FeaturesSettings.vue +++ b/apps/app-frontend/src/components/ui/settings/display/FeaturesSettings.vue @@ -82,32 +82,6 @@ const messages = defineMessages({ id: 'app.features-settings.sidebar.title', defaultMessage: 'Sidebar', }, - showAllScreenshotsTitle: { - id: 'app.features-settings.show-all-screenshots.title', - defaultMessage: 'Show all screenshots in sidebar', - }, - showAllScreenshotsDescription: { - id: 'app.features-settings.show-all-screenshots.description', - defaultMessage: - 'Show a button in the left sidebar to view screenshots from all your instances.', - }, - showSkinSelectorTitle: { - id: 'app.features-settings.show-skin-selector.title', - defaultMessage: 'Show skin selector in sidebar', - }, - showSkinSelectorDescription: { - id: 'app.features-settings.show-skin-selector.description', - defaultMessage: 'Show a button in the left sidebar to open the skin selector.', - }, - showHostingTitle: { - id: 'app.features-settings.show-hosting.title', - defaultMessage: 'Show Modrinth Servers in sidebar', - }, - showHostingDescription: { - id: 'app.features-settings.show-hosting.description', - defaultMessage: - 'Show a button in the left sidebar to manage Modrinth Servers. Added by Modrinth Enhanced, so it stays on this device and is not synced to your Modrinth account.', - }, quickInstancesTitle: { id: 'app.features-settings.quick-instances.title', defaultMessage: 'Quick instances in sidebar', @@ -384,50 +358,6 @@ onBeforeUnmount(() => { {{ formatMessage(messages.sidebarTitle) }}
-
-
-

- {{ formatMessage(messages.showAllScreenshotsTitle) }} -

-

- {{ formatMessage(messages.showAllScreenshotsDescription) }} -

-
- -
- -
-
-

- {{ formatMessage(messages.showSkinSelectorTitle) }} -

-

{{ formatMessage(messages.showSkinSelectorDescription) }}

-
- -
- -
-
-

- {{ formatMessage(messages.showHostingTitle) }} -

-

{{ formatMessage(messages.showHostingDescription) }}

-
- -
-

{{ formatMessage(messages.quickInstancesTitle) }} diff --git a/apps/app-frontend/src/components/ui/settings/display/SidebarSettings.vue b/apps/app-frontend/src/components/ui/settings/display/SidebarSettings.vue new file mode 100644 index 0000000..dc0cd1f --- /dev/null +++ b/apps/app-frontend/src/components/ui/settings/display/SidebarSettings.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/apps/app-frontend/src/composables/use-sidebar-layout.ts b/apps/app-frontend/src/composables/use-sidebar-layout.ts new file mode 100644 index 0000000..7225c21 --- /dev/null +++ b/apps/app-frontend/src/composables/use-sidebar-layout.ts @@ -0,0 +1,72 @@ +/** + * The order of the buttons above the instances in the left sidebar, and which + * of them are shown. + * + * This is about how the app looks on this machine, so it stays here rather than + * going to Modrinth with the synced preferences. + */ +import { useStorage } from '@vueuse/core' +import { computed } from 'vue' + +/** The buttons, in the order upstream has them. */ +export const SIDEBAR_ITEMS = ['home', 'discover', 'skins', 'screenshots', 'hosting'] as const + +export type SidebarItemId = (typeof SIDEBAR_ITEMS)[number] + +type SidebarLayout = { + order: SidebarItemId[] + hidden: SidebarItemId[] +} + +const layout = useStorage('modrinth-enhanced:sidebar-layout', { + order: [...SIDEBAR_ITEMS], + hidden: [], +}) + +/** The stored order, with anything unknown dropped and anything new appended. */ +const order = computed(() => { + const stored = (layout.value.order ?? []).filter((id) => SIDEBAR_ITEMS.includes(id)) + return [...stored, ...SIDEBAR_ITEMS.filter((id) => !stored.includes(id))] +}) + +const hidden = computed(() => + (layout.value.hidden ?? []).filter((id) => SIDEBAR_ITEMS.includes(id)), +) + +export function useSidebarLayout() { + function isHidden(id: SidebarItemId): boolean { + return hidden.value.includes(id) + } + + function setHidden(id: SidebarItemId, hide: boolean) { + layout.value = { + order: order.value, + hidden: hide ? [...hidden.value, id] : hidden.value.filter((other) => other !== id), + } + } + + /** Moves a button by `by` places, as far as there is room. */ + function move(id: SidebarItemId, by: number) { + const ids = [...order.value] + const from = ids.indexOf(id) + const to = from + by + if (from < 0 || to < 0 || to >= ids.length) return + + ids.splice(to, 0, ...ids.splice(from, 1)) + layout.value = { order: ids, hidden: hidden.value } + } + + /** The order the buttons were dragged into, anything unknown dropped. */ + function setOrder(ids: SidebarItemId[]) { + layout.value = { + order: ids.filter((id) => SIDEBAR_ITEMS.includes(id)), + hidden: hidden.value, + } + } + + function reset() { + layout.value = { order: [...SIDEBAR_ITEMS], hidden: [] } + } + + return { order, hidden, isHidden, setHidden, move, setOrder, reset } +}