From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Modrinth Enhanced Date: Mon, 14 Sep 2026 13:06:38 +0200 Subject: [PATCH] Make the sidebars foldable Three sidebar preferences, all stored as feature flags so they survive a restart without needing a settings migration: * The Modrinth Servers button in the left sidebar can be switched off under Settings > Features > Sidebar. It stays on by default. * The news section in the right sidebar collapses by clicking its heading, remembering the state the way the friends list does. * The button that folds the right sidebar away is always in the title bar, rather than appearing only once "Hide right sidebar" is turned on in settings, and what it is set to is remembered. That setting still works, and still seeds the state. Its label was pointing at the wrong message ("Next image"), which is fixed here too. Once the fold state is saved, a check pops onto the button and its tooltip says it is remembered after a restart, since nothing else shows that. On pages that need the sidebar (browse, projects, users) the button stays in place, disabled, instead of vanishing and shifting the title bar. None of these three reach Modrinth. Preference syncing maps a fixed list of named fields in both directions, and these are not in it, so they are neither sent nor overwritten by another device. The Modrinth Servers switch says so where it sits, because it sits on a page headed "Sync features across devices" and would otherwise look like it syncs. The other two are not settings entries - a heading and a title bar button - so nothing there promises anything. --- apps/app-frontend/src/App.vue | 184 ++++++++++++++++-- .../ui/settings/display/FeaturesSettings.vue | 28 +++ .../src/composables/use-app-settings.ts | 4 + packages/app-lib/src/state/settings.rs | 4 + 4 files changed, 205 insertions(+), 15 deletions(-) diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index d750a5f..c47484d 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -10,6 +10,8 @@ import { } from '@modrinth/api-client' import { ArrowLeftRightIcon, + CheckIcon, + ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, CompassIcon, @@ -34,6 +36,7 @@ import { AccountSwitchOverlay, Admonition, Avatar, + Button, ButtonLink, commonMessages, commonSettingsMessages, @@ -44,6 +47,7 @@ import { I18nDebugPanel, IconButton, LoadingBar, + NewModal, NewsArticleCard, NotificationPanel, PopupNotificationPanel, @@ -254,12 +258,46 @@ const credentials = ref() const storedModrinthAccounts = ref([]) let credentialsRefreshId = 0 const sidebarToggled = ref(true) +// Whether the right sidebar is folded away, remembered across restarts in the +// feature flags. The title bar button below is what normally sets it; the +// "Hide right sidebar" setting still seeds it, so turning that on folds the +// sidebar away as it always did. watch( - () => appSettings.toggleSidebar, - (toggleSidebar) => { - sidebarToggled.value = !toggleSidebar + () => appSettings.getFeatureFlag('right_sidebar_collapsed'), + (collapsed) => { + sidebarToggled.value = !collapsed }, + { immediate: true }, +) +watch( + () => appSettings.toggleSidebar, + (hide) => setSidebarCollapsed(hide), ) + +// Briefly shows a check on the fold button once the state is saved, and the +// first time also explains that the choice is remembered. +const sidebarSaved = ref(false) +let sidebarSavedTimeout +const sidebarRememberedModal = ref(null) + +function setSidebarCollapsed(collapsed) { + const explain = !appSettings.featureFlags.sidebar_fold_explained + appSettings.featureFlags.right_sidebar_collapsed = collapsed + appSettings.featureFlags.sidebar_fold_explained = true + getSettings() + .then((settings) => { + settings.feature_flags.right_sidebar_collapsed = collapsed + settings.feature_flags.sidebar_fold_explained = true + return setSettings(settings) + }) + .then(() => { + sidebarSaved.value = true + clearTimeout(sidebarSavedTimeout) + sidebarSavedTimeout = setTimeout(() => (sidebarSaved.value = false), 1600) + if (explain) sidebarRememberedModal.value?.show() + }) + .catch(handleError) +} const forceSidebar = computed( () => route.path.startsWith('/browse') || @@ -358,6 +396,21 @@ useQuery({ // ad and the gradient being drawn above it. const showAd = computed(() => false) const adConsentAvailable = computed(() => false) + +// Whether the news section in the right sidebar is folded away. Kept in the +// feature flags so it survives a restart, the same way the friends list +// remembers its collapsed sections. +const newsCollapsed = computed(() => appSettings.getFeatureFlag('news_collapsed')) + +function setNewsCollapsed(collapsed) { + appSettings.featureFlags.news_collapsed = collapsed + getSettings() + .then((settings) => { + settings.feature_flags.news_collapsed = collapsed + return setSettings(settings) + }) + .catch(handleError) +} providePageContext({ hierarchicalSidebarAvailable: ref(true), showAds: showAd, @@ -588,6 +641,25 @@ const messages = defineMessages({ goBack: { id: 'app.navigation.go-back', defaultMessage: 'Go back' }, goForward: { id: 'app.navigation.go-forward', defaultMessage: 'Go forward' }, nextImage: { id: 'app.navigation.next-image', defaultMessage: 'Next image' }, + hideSidebar: { id: 'app.navigation.hide-sidebar', defaultMessage: 'Hide sidebar' }, + showSidebar: { id: 'app.navigation.show-sidebar', defaultMessage: 'Show sidebar' }, + sidebarRememberedTitle: { + id: 'app.navigation.sidebar-remembered.title', + defaultMessage: 'The sidebar stays how you leave it', + }, + sidebarRememberedBody: { + id: 'app.navigation.sidebar-remembered.body', + defaultMessage: + 'Folding the sidebar away or bringing it back is saved right away, so the app opens the same way next time, even after a restart. The check on the button shows when it has been saved.', + }, + sidebarRememberedDone: { + id: 'app.navigation.sidebar-remembered.done', + defaultMessage: 'Done', + }, + sidebarRequired: { + id: 'app.navigation.sidebar-required', + defaultMessage: 'This page needs the sidebar', + }, updateDownloadMissingVersion: { id: 'app.update.download-error.missing-version', defaultMessage: 'Failed to download update: no version available', @@ -2252,6 +2324,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
- + - - + + + + + + + + +
@@ -2502,10 +2601,18 @@ provideAppUpdateDownloadProgress(appUpdateDownload) class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid" />
-

+

-
+ + +
+ +

{{ formatMessage(messages.sidebarRememberedBody) }}

+ +
@@ -2743,6 +2865,38 @@ provideAppUpdateDownloadProgress(appUpdateDownload) display: contents; } +@media (prefers-reduced-motion: no-preference) { + .sidebar-saved-enter-active { + transition: transform 0.35s cubic-bezier(0.15, 1.4, 0.64, 0.96); + } + + .sidebar-saved-leave-active { + transition: + transform 0.2s ease, + opacity 0.2s ease; + } + + .sidebar-saved-enter-from, + .sidebar-saved-leave-to { + transform: scale(0); + opacity: 0; + } + + /* The path runs from the long stroke's tip back to the short one, so it is + revealed from its end: the check is drawn left to right. */ + .sidebar-saved-badge :deep(path) { + stroke-dasharray: 24; + stroke-dashoffset: -24; + animation: sidebar-saved-draw 0.3s 0.15s ease-out forwards; + } +} + +@keyframes sidebar-saved-draw { + to { + stroke-dashoffset: 0; + } +} + @media (prefers-reduced-motion: no-preference) { .nav-button-animated-enter-active { transition: all 0.5s cubic-bezier(0.15, 1.4, 0.64, 0.96); 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 1d3e379..ae88550 100644 --- a/apps/app-frontend/src/components/ui/settings/display/FeaturesSettings.vue +++ b/apps/app-frontend/src/components/ui/settings/display/FeaturesSettings.vue @@ -43,6 +43,7 @@ const quickInstances = useQuickInstanceLimit() const queryClient = useQueryClient() const showJumpInFlag: FeatureFlag = 'worlds_in_home' +const showHostingFlag: FeatureFlag = 'show_hosting_in_sidebar' const messages = defineMessages({ syncAcrossDevicesTitle: { @@ -95,6 +96,15 @@ const messages = defineMessages({ 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', @@ -125,6 +135,7 @@ type FeaturesSettingsState = { showScreenshotsTab: boolean showAllScreenshots: boolean showSkinSelector: boolean + showHosting: boolean quickInstanceCount: number showJumpIn: boolean } @@ -144,6 +155,7 @@ function getFeaturesSettingsState( showScreenshotsTab: settings.show_screenshots_tab_in_instances, showAllScreenshots: globalSyncedOptions.screenshots, showSkinSelector: settings.show_skin_selector_in_sidebar, + showHosting: settings.feature_flags[showHostingFlag] ?? DEFAULT_FEATURE_FLAGS[showHostingFlag], quickInstanceCount: quickInstances.limit.value ?? QUICK_INSTANCE_LIMIT_MAX, showJumpIn: settings.feature_flags[showJumpInFlag] ?? DEFAULT_FEATURE_FLAGS[showJumpInFlag], } @@ -184,6 +196,7 @@ const settingsMutation = useMutation({ feature_flags: { ...latestSettings.feature_flags, [showJumpInFlag]: value.showJumpIn, + [showHostingFlag]: value.showHosting, }, } @@ -206,6 +219,7 @@ const settingsMutation = useMutation({ appSettings.showScreenshotsTabInInstances = value.showScreenshotsTab appSettings.showSkinSelectorInSidebar = value.showSkinSelector appSettings.featureFlags[showJumpInFlag] = value.showJumpIn + appSettings.featureFlags[showHostingFlag] = value.showHosting if (updateQuickInstanceCount) { quickInstances.setLimit(value.quickInstanceCount) @@ -366,6 +380,20 @@ onBeforeUnmount(() => { />
+
+
+

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

+

{{ formatMessage(messages.showHostingDescription) }}

+
+ +
+

{{ formatMessage(messages.quickInstancesTitle) }} diff --git a/apps/app-frontend/src/composables/use-app-settings.ts b/apps/app-frontend/src/composables/use-app-settings.ts index 0d58903..28c78c3 100644 --- a/apps/app-frontend/src/composables/use-app-settings.ts +++ b/apps/app-frontend/src/composables/use-app-settings.ts @@ -24,6 +24,10 @@ export const DEFAULT_FEATURE_FLAGS = { friends_pending_collapsed: true, dismissed_photosensitivity_filter_warning: false, localhost_sign_in: false, + show_hosting_in_sidebar: true, + news_collapsed: false, + sidebar_fold_explained: false, + right_sidebar_collapsed: false, } export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS diff --git a/packages/app-lib/src/state/settings.rs b/packages/app-lib/src/state/settings.rs index 20601b8..b5f2ec6 100644 --- a/packages/app-lib/src/state/settings.rs +++ b/packages/app-lib/src/state/settings.rs @@ -88,6 +88,10 @@ pub enum FeatureFlag { FriendsPendingCollapsed, DismissedPhotosensitivityFilterWarning, LocalhostSignIn, + ShowHostingInSidebar, + NewsCollapsed, + SidebarFoldExplained, + RightSidebarCollapsed, } impl Settings {