453 lines
17 KiB
Diff
453 lines
17 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: Modrinth Enhanced <patches@modrinth-enhanced.invalid>
|
|
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 | 180 ++++++++++++++++--
|
|
.../ui/settings/display/BehaviorSettings.vue | 18 --
|
|
.../ui/settings/display/FeaturesSettings.vue | 28 +++
|
|
.../src/composables/use-app-settings.ts | 4 +
|
|
packages/app-lib/src/state/settings.rs | 4 +
|
|
5 files changed, 201 insertions(+), 33 deletions(-)
|
|
|
|
diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue
|
|
index d750a5f..76da736 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,42 @@ 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 the only thing that sets it:
|
|
+// upstream's "Hide right sidebar" setting, which used to be what showed that
|
|
+// button, is gone.
|
|
watch(
|
|
- () => appSettings.toggleSidebar,
|
|
- (toggleSidebar) => {
|
|
- sidebarToggled.value = !toggleSidebar
|
|
+ () => appSettings.getFeatureFlag('right_sidebar_collapsed'),
|
|
+ (collapsed) => {
|
|
+ sidebarToggled.value = !collapsed
|
|
},
|
|
+ { immediate: true },
|
|
)
|
|
+
|
|
+// 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 +392,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 +637,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 +2320,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
|
<ImageIcon />
|
|
</NavButton>
|
|
<NavButton
|
|
+ v-if="appSettings.getFeatureFlag('show_hosting_in_sidebar')"
|
|
v-tooltip.right="formatMessage(messages.modrinthHosting)"
|
|
to="/hosting/manage"
|
|
:is-primary="(r) => r.path === '/hosting/manage' || r.path === '/hosting/manage/'"
|
|
@@ -2377,16 +2446,42 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
|
<Breadcrumbs />
|
|
</div>
|
|
<section data-tauri-drag-region class="flex shrink-0 ml-auto items-center">
|
|
- <IconButton
|
|
- v-if="!forceSidebar && appSettings.toggleSidebar"
|
|
- :type="sidebarToggled ? 'base' : 'quiet'"
|
|
- :label="formatMessage(messages.nextImage)"
|
|
- class="mr-3 transition-transform"
|
|
- :class="{ 'rotate-180': !sidebarToggled }"
|
|
- @click="sidebarToggled = !sidebarToggled"
|
|
+ <!--
|
|
+ Stays in place on pages that need the sidebar, so the title bar does
|
|
+ not shift. The tooltip sits on the wrapper since a disabled button
|
|
+ gets no hover events.
|
|
+ -->
|
|
+ <span
|
|
+ v-tooltip="
|
|
+ formatMessage(
|
|
+ forceSidebar
|
|
+ ? messages.sidebarRequired
|
|
+ : sidebarToggled
|
|
+ ? messages.hideSidebar
|
|
+ : messages.showSidebar,
|
|
+ )
|
|
+ "
|
|
+ class="relative mr-3 flex"
|
|
>
|
|
- <RightArrowIcon />
|
|
- </IconButton>
|
|
+ <IconButton
|
|
+ :type="sidebarVisible ? 'base' : 'quiet'"
|
|
+ :label="formatMessage(sidebarToggled ? messages.hideSidebar : messages.showSidebar)"
|
|
+ :disabled="forceSidebar"
|
|
+ class="transition-transform"
|
|
+ :class="{ 'rotate-180': !sidebarVisible }"
|
|
+ @click="setSidebarCollapsed(sidebarToggled)"
|
|
+ >
|
|
+ <RightArrowIcon />
|
|
+ </IconButton>
|
|
+ <Transition name="sidebar-saved">
|
|
+ <span
|
|
+ v-if="sidebarSaved && !forceSidebar"
|
|
+ class="sidebar-saved-badge pointer-events-none absolute -right-1 -top-1 flex size-4 items-center justify-center rounded-full bg-brand text-brand-inverted"
|
|
+ >
|
|
+ <CheckIcon class="size-3" stroke-width="3" />
|
|
+ </span>
|
|
+ </Transition>
|
|
+ </span>
|
|
<div class="flex mr-3">
|
|
<Suspense>
|
|
<AppActionBar />
|
|
@@ -2502,10 +2597,18 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
|
class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid"
|
|
/>
|
|
<div v-if="news && news.length > 0" class="p-4 flex flex-col items-center">
|
|
- <h3 class="text-base mb-4 text-primary font-medium m-0 text-left w-full">
|
|
+ <button
|
|
+ class="button-base m-0 mb-4 flex w-full cursor-pointer items-center justify-between gap-2 border-0 bg-transparent p-0 text-left text-base font-medium text-primary"
|
|
+ :aria-expanded="!newsCollapsed"
|
|
+ @click="setNewsCollapsed(!newsCollapsed)"
|
|
+ >
|
|
{{ formatMessage(messages.news) }}
|
|
- </h3>
|
|
- <div class="space-y-4 flex flex-col items-center w-full">
|
|
+ <ChevronDownIcon
|
|
+ class="h-5 w-5 shrink-0 transition-transform"
|
|
+ :class="{ '-rotate-90': newsCollapsed }"
|
|
+ />
|
|
+ </button>
|
|
+ <div v-if="!newsCollapsed" class="space-y-4 flex flex-col items-center w-full">
|
|
<NewsArticleCard
|
|
v-for="(item, index) in news"
|
|
:key="`news-${index}`"
|
|
@@ -2531,6 +2634,21 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
|
<I18nDebugPanel />
|
|
<NotificationPanel :has-sidebar="sidebarVisible" />
|
|
<PopupNotificationPanel :has-sidebar="sidebarVisible" />
|
|
+ <NewModal
|
|
+ ref="sidebarRememberedModal"
|
|
+ :header="formatMessage(messages.sidebarRememberedTitle)"
|
|
+ max-width="460px"
|
|
+ >
|
|
+ <p class="m-0">{{ formatMessage(messages.sidebarRememberedBody) }}</p>
|
|
+ <template #actions>
|
|
+ <div class="flex justify-end">
|
|
+ <Button type="colored" color="brand" @click="sidebarRememberedModal?.hide()">
|
|
+ <CheckIcon />
|
|
+ {{ formatMessage(messages.sidebarRememberedDone) }}
|
|
+ </Button>
|
|
+ </div>
|
|
+ </template>
|
|
+ </NewModal>
|
|
<ErrorModal ref="errorModal" />
|
|
<MinecraftAuthErrorModal ref="minecraftAuthErrorModal" />
|
|
<MinecraftRequiredModal ref="minecraftRequiredModal" />
|
|
@@ -2743,6 +2861,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/BehaviorSettings.vue b/apps/app-frontend/src/components/ui/settings/display/BehaviorSettings.vue
|
|
index 55622d7..6c6ba20 100644
|
|
--- a/apps/app-frontend/src/components/ui/settings/display/BehaviorSettings.vue
|
|
+++ b/apps/app-frontend/src/components/ui/settings/display/BehaviorSettings.vue
|
|
@@ -80,14 +80,6 @@ const messages = defineMessages({
|
|
id: 'app.appearance-settings.default-landing-page.library',
|
|
defaultMessage: 'Library',
|
|
},
|
|
- toggleSidebarTitle: {
|
|
- id: 'app.appearance-settings.toggle-sidebar.title',
|
|
- defaultMessage: 'Hide right sidebar',
|
|
- },
|
|
- toggleSidebarDescription: {
|
|
- id: 'app.appearance-settings.toggle-sidebar.description',
|
|
- defaultMessage: 'Hide the right sidebar by default and add a button to show or hide it.',
|
|
- },
|
|
compactModeTitle: {
|
|
id: 'app.appearance-settings.compact-mode.title',
|
|
defaultMessage: 'Compact mode',
|
|
@@ -288,16 +280,6 @@ onBeforeUnmount(() => {
|
|
</div>
|
|
<Toggle id="minimize-launcher" v-model="current.minimizeApp" />
|
|
</div>
|
|
-
|
|
- <div class="flex items-center justify-between gap-4">
|
|
- <div>
|
|
- <h3 class="m-0 text-lg font-semibold text-contrast">
|
|
- {{ formatMessage(messages.toggleSidebarTitle) }}
|
|
- </h3>
|
|
- <p class="m-0 mt-1">{{ formatMessage(messages.toggleSidebarDescription) }}</p>
|
|
- </div>
|
|
- <Toggle id="toggle-sidebar" v-model="current.hideRightSidebar" />
|
|
- </div>
|
|
</div>
|
|
</section>
|
|
|
|
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(() => {
|
|
/>
|
|
</div>
|
|
|
|
+ <div class="flex items-center justify-between gap-4">
|
|
+ <div>
|
|
+ <h3 class="m-0 text-lg font-semibold text-contrast">
|
|
+ {{ formatMessage(messages.showHostingTitle) }}
|
|
+ </h3>
|
|
+ <p class="m-0 mt-1">{{ formatMessage(messages.showHostingDescription) }}</p>
|
|
+ </div>
|
|
+ <Toggle
|
|
+ id="show-hosting-in-sidebar"
|
|
+ v-model="current.showHosting"
|
|
+ :aria-label="formatMessage(messages.showHostingTitle)"
|
|
+ />
|
|
+ </div>
|
|
+
|
|
<div class="flex flex-col gap-2.5">
|
|
<h3 class="m-0 text-lg font-semibold text-contrast">
|
|
{{ 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 {
|