diff --git a/README.md b/README.md index 5245cc8..efd0d19 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ sudo dnf install "$(curl -fsSL https://api.github.com/repos/Felitendo/Modrinth-E | `0022-Sign-in-to-custom-servers-in-...` | Servers with Yggdrasil Connect, such as LittleSkin, sign in on their own page, with two-factor authentication. | | `0023-Browse-LittleSkin-s-skin-library` | LittleSkin's skin library in the Browse tab, with search, sorting and likes. | | `0024-Offer-the-launchers-instances-...` | Every launcher the importers read is offered, with the folder it keeps instances in. | +| `0025-Arrange-the-left-sidebar` | The buttons in the left sidebar are reordered and hidden in a settings tab of their own. | ### Offline accounts @@ -128,8 +129,10 @@ on the server's website: it only knows the one uploaded there, and taking it off ### Sidebar and news -The Modrinth Servers button in the left sidebar can be switched off under -Settings > Features > Sidebar. +Settings > Sidebar has the buttons above the instances — Home, Discover, Skins, Screenshots and +Modrinth Hosting — in a list: move one up or down, hide it, or put the usual order back. +Right-clicking one of them in the sidebar hides it or opens that tab. Where it all sits is kept on +this machine, since it is about how the app looks here. The news section in the right sidebar folds away by clicking its heading. The right sidebar itself folds away with the arrow button in the title bar. Upstream shows that button only once "Hide right diff --git a/patches/0025-Arrange-the-left-sidebar.patch b/patches/0025-Arrange-the-left-sidebar.patch new file mode 100644 index 0000000..31b276c --- /dev/null +++ b/patches/0025-Arrange-the-left-sidebar.patch @@ -0,0 +1,583 @@ +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 | 64 ++++++++ + 5 files changed, 302 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 6539d4a..2bd8eb3 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', +@@ -2327,52 +2409,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]" + > + +- +- +- +- +- +- +- +- +- +- +- +- +- ++ + + + +@@ -2456,6 +2501,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 9a9e8ef..7636c46 100644 +--- a/apps/app-frontend/src/components/ui/settings/display/FeaturesSettings.vue ++++ b/apps/app-frontend/src/components/ui/settings/display/FeaturesSettings.vue +@@ -83,32 +83,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', +@@ -385,50 +359,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..54e155a +--- /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..8d12b8d +--- /dev/null ++++ b/apps/app-frontend/src/composables/use-sidebar-layout.ts +@@ -0,0 +1,64 @@ ++/** ++ * 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 } ++ } ++ ++ function reset() { ++ layout.value = { order: [...SIDEBAR_ITEMS], hidden: [] } ++ } ++ ++ return { order, hidden, isHidden, setHidden, move, reset } ++} diff --git a/scripts/check.sh b/scripts/check.sh index 6f4de57..21947e7 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -170,8 +170,12 @@ check "the skin page has a Browse tab" \ contains "$WORKTREE/apps/app-frontend/src/pages/Skins.vue" '