Files
modrinth/apps/frontend/src/pages/settings/index.vue
T
Prospector b68a199814 menu refactor + web account switcher (#7307)
* add account switcher, migrate context menus to overflow menu system, update version filter controls, only pad instance icons

* app account switcher, improve some context menus, theme stuff

* prepr

* handle reauthentication

* fix a couple sign in issues, admin page cleanup, fix org status badges, fix official account badge, open in local keybind, publish plus icons

* rename generic menus to ButtonMenu (& types)

* fix hydration issue w/ dropdowns + middleware error
2026-08-27 00:35:34 +00:00

329 lines
9.1 KiB
Vue

<template>
<div>
<Admonition v-if="flags.developerMode" type="critical" class="mb-4" show-actions-underneath>
<template #icon="{ iconClass }">
<CodeIcon :class="iconClass" aria-hidden="true" />
</template>
<IntlFormatted :message-id="developerModeBanner.description">
<template #strong="{ children }">
<strong>
<component :is="() => normalizeChildren(children)" />
</strong>
</template>
</IntlFormatted>
<template #actions>
<Button type="colored" color="red" @click="disableDeveloperMode()">
{{ formatMessage(developerModeBanner.deactivate) }}
</Button>
</template>
</Admonition>
<section class="universal-card">
<AppearanceSettingsLayout />
</section>
<UnsavedChangesPopup
:original="saved"
:modified="changes"
:saving="saving"
@reset="reset"
@save="saveAppearanceSettings"
/>
</div>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { CodeIcon } from '@modrinth/assets'
import {
Admonition,
AppearanceSettingsLayout,
Button,
defineMessages,
injectAuth,
injectNotificationManager,
injectUserPreferences,
IntlFormatted,
isProjectDisplayLocation,
normalizeChildren,
type ProjectDisplayLocation,
type ProjectLayout,
type ProjectLayoutSetting,
provideAppearanceSettings,
type SidebarPreferences,
UnsavedChangesPopup,
useSavable,
useVIntl,
} from '@modrinth/ui'
import { isDarkTheme, type Theme } from '~/plugins/theme/index.ts'
const { addNotification } = injectNotificationManager()
const auth = injectAuth()
const { updatePreferences } = injectUserPreferences()
const { formatMessage } = useVIntl()
const messages = defineMessages({
headTitle: {
id: 'settings.head-title',
defaultMessage: 'Display settings',
},
})
const developerModeBanner = defineMessages({
description: {
id: 'settings.display.banner.developer-mode.description',
defaultMessage:
"<strong>Developer mode</strong> is active. This will allow you to view the internal IDs of various things throughout Modrinth that may be helpful if you're a developer using the Modrinth API. Click on the Modrinth logo at the bottom of the page 5 times to toggle developer mode.",
},
deactivate: {
id: 'settings.display.banner.developer-mode.button',
defaultMessage: 'Deactivate developer mode',
},
})
useHead({
title: () => `${formatMessage(messages.headTitle)} - Modrinth`,
})
const notifications = defineMessages({
developerModeDeactivatedTitle: {
id: 'settings.display.notification.developer-mode-deactivated.title',
defaultMessage: 'Developer mode deactivated',
},
developerModeDeactivatedText: {
id: 'settings.display.notification.developer-mode-deactivated.text',
defaultMessage: 'Developer mode has been disabled',
},
})
const cosmetics = useCosmetics()
const flags = useFeatureFlags()
const tags = useGeneratedState()
const theme = useTheme()
// On the server the value of native theme can be 'unknown'. To hydrate
// correctly, we need to make sure we aren't using 'unknown' and values between
// server and client renders are in sync.
const serverSystemTheme = useState(() => {
const theme_ = theme.native
if (theme_ === 'unknown') return 'light'
return theme_
})
const systemTheme = useMountedValue((mounted): Theme => {
const systemTheme_ = mounted ? theme.native : serverSystemTheme.value
return systemTheme_ === 'light' ? theme.preferences.light : theme.preferences.dark
})
const projectLayouts = computed<ProjectLayoutSetting[]>(() => {
const layouts = tags.value.projectTypes
.map(({ id }) => id)
.filter(isProjectDisplayLocation)
.map(
(type): ProjectLayoutSetting => ({
type,
layout: cosmetics.value.searchDisplayMode[type] === 'list' ? 'rows' : 'grid',
}),
)
layouts.push({
type: 'user',
layout: cosmetics.value.searchDisplayMode.user === 'list' ? 'rows' : 'grid',
})
return layouts
})
const sidebarPreferences = computed<SidebarPreferences>(() => ({
right_aligned_search: cosmetics.value.rightSearchLayout,
left_aligned_content: cosmetics.value.leftContentLayout,
}))
type AppearanceSettingsState = {
theme: Theme | 'system'
syncAcrossDevices: boolean
advancedRendering: boolean
projectLayouts: ProjectLayoutSetting[]
externalLinksNewTab: boolean
sidebarPreferences: SidebarPreferences
}
const layoutPreferenceKeys: Record<
ProjectDisplayLocation,
keyof Labrinth.Users.v3.LayoutPreferences
> = {
mod: 'mods',
plugin: 'plugins',
datapack: 'datapacks',
shader: 'shaders',
resourcepack: 'resourcepacks',
modpack: 'modpacks',
server: 'servers',
user: 'users',
}
function getAppearanceSettingsState(): AppearanceSettingsState {
return {
theme: theme.preferred,
syncAcrossDevices: theme.syncAcrossDevices,
advancedRendering: cosmetics.value.advancedRendering,
projectLayouts: projectLayouts.value,
externalLinksNewTab: cosmetics.value.externalLinksNewTab,
sidebarPreferences: sidebarPreferences.value,
}
}
const { saved, current, changes, saving, reset, save } = useSavable(
getAppearanceSettingsState,
async (appearanceChanges) => {
const value = current.value
const preferencesPatch: Labrinth.Users.v3.PartialUserPreferences = {}
if (
value.syncAcrossDevices &&
auth.user.value &&
(appearanceChanges.theme !== undefined || appearanceChanges.syncAcrossDevices !== undefined)
) {
preferencesPatch.appearance =
value.theme === 'system' ? { auto: true } : { auto: false, theme: value.theme }
}
if (appearanceChanges.projectLayouts !== undefined) {
const layouts: Partial<Labrinth.Users.v3.LayoutPreferences> = {}
for (const setting of value.projectLayouts) {
layouts[layoutPreferenceKeys[setting.type]] = setting.layout
}
preferencesPatch.layouts = layouts
}
if (appearanceChanges.sidebarPreferences !== undefined) {
preferencesPatch.sidebars = value.sidebarPreferences
}
if (Object.keys(preferencesPatch).length > 0) {
await updatePreferences(preferencesPatch)
}
if (value.theme !== 'system') {
if (isDarkTheme(value.theme)) {
theme.preferences.dark = value.theme
} else {
theme.preferences.light = value.theme
}
}
theme.preferred = value.theme
theme.syncAcrossDevices = value.syncAcrossDevices
cosmetics.value.advancedRendering = value.advancedRendering
cosmetics.value.externalLinksNewTab = value.externalLinksNewTab
cosmetics.value.rightSearchLayout = value.sidebarPreferences.right_aligned_search
cosmetics.value.leftContentLayout = value.sidebarPreferences.left_aligned_content
for (const setting of value.projectLayouts) {
cosmetics.value.searchDisplayMode[setting.type] = setting.layout === 'rows' ? 'list' : 'grid'
}
},
)
const themeOptions = computed(() => {
const options: ('system' | Theme)[] = ['system', 'light', 'dark', 'oled']
if (flags.value.developerMode || current.value.theme === 'retro') {
options.push('retro')
}
return options
})
function setTheme(value: Theme | 'system'): void {
current.value.theme = value
}
function setSyncAcrossDevices(value: boolean): void {
current.value.syncAcrossDevices = value
}
function setAdvancedRendering(value: boolean): void {
current.value.advancedRendering = value
}
function setProjectLayout(type: ProjectDisplayLocation, layout: ProjectLayout): void {
current.value.projectLayouts = current.value.projectLayouts.map((setting) =>
setting.type === type ? { ...setting, layout } : setting,
)
}
function setExternalLinksNewTab(value: boolean): void {
current.value.externalLinksNewTab = value
}
function setSidebarPreference(key: keyof SidebarPreferences, value: boolean): void {
current.value.sidebarPreferences = {
...current.value.sidebarPreferences,
[key]: value,
}
}
watch(
[() => current.value.theme, () => saved.value.theme],
([selectedTheme, savedTheme]) => {
theme.preview = selectedTheme === savedTheme ? null : selectedTheme
},
{ immediate: true },
)
onBeforeUnmount(() => {
theme.preview = null
})
async function saveAppearanceSettings(): Promise<void> {
try {
await save()
} catch {
return
}
}
provideAppearanceSettings({
deferPersistence: true,
theme: {
current: computed(() => current.value.theme),
options: themeOptions,
system: systemTheme,
preferredDark: computed(() =>
isDarkTheme(current.value.theme) ? current.value.theme : theme.preferences.dark,
),
set: setTheme,
syncAcrossDevices: {
value: computed(() => current.value.syncAcrossDevices),
set: setSyncAcrossDevices,
},
syncDisabled: computed(() => !auth.user.value),
},
advancedRendering: {
value: computed(() => current.value.advancedRendering),
set: setAdvancedRendering,
},
projectLayouts: {
value: computed(() => current.value.projectLayouts),
set: setProjectLayout,
},
externalLinksNewTab: {
value: computed(() => current.value.externalLinksNewTab),
set: setExternalLinksNewTab,
},
sidebarPreferences: {
value: computed(() => current.value.sidebarPreferences),
set: setSidebarPreference,
},
updatePreferences,
})
function disableDeveloperMode(): void {
flags.value.developerMode = !flags.value.developerMode
saveFeatureFlags()
addNotification({
title: formatMessage(notifications.developerModeDeactivatedTitle),
text: formatMessage(notifications.developerModeDeactivatedText),
type: 'success',
})
}
</script>