refactor: app settings layout (#6891)

* refactor: app settings layout

* fix: privacy copy got reverted

* fix: i18n

* fix: modal title

* fix: rev comments
This commit is contained in:
Calum H.
2026-07-28 14:09:41 +00:00
committed by GitHub
parent 3c8bb0923d
commit 5d34e7902a
50 changed files with 1645 additions and 961 deletions
@@ -1,5 +1,9 @@
<template> <template>
<ModalWrapper ref="detectJavaModal" header="Select java version" :show-ad-on-close="false"> <ModalWrapper
ref="detectJavaModal"
:header="formatMessage(messages.title)"
:show-ad-on-close="false"
>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<Table :columns="javaInstallColumns" :data="chosenInstallOptions" row-key="path"> <Table :columns="javaInstallColumns" :data="chosenInstallOptions" row-key="path">
<template #cell-version="{ value }"> <template #cell-version="{ value }">
@@ -11,15 +15,23 @@
<template #cell-actions="{ row }"> <template #cell-actions="{ row }">
<div class="flex items-center justify-end"> <div class="flex items-center justify-end">
<ButtonStyled v-if="currentSelected.path === row.path"> <ButtonStyled v-if="currentSelected.path === row.path">
<button class="!shadow-none" disabled><CheckIcon /> Selected</button> <button class="!shadow-none" disabled>
<CheckIcon aria-hidden="true" />
{{ formatMessage(messages.selected) }}
</button>
</ButtonStyled> </ButtonStyled>
<ButtonStyled v-else> <ButtonStyled v-else>
<button class="!shadow-none" @click="setJavaInstall(row)"><PlusIcon /> Select</button> <button class="!shadow-none" @click="setJavaInstall(row)">
<PlusIcon aria-hidden="true" />
{{ formatMessage(messages.select) }}
</button>
</ButtonStyled> </ButtonStyled>
</div> </div>
</template> </template>
<template #empty-state> <template #empty-state>
<div class="p-4 text-secondary">No java installations found!</div> <div class="p-4 text-secondary">
{{ formatMessage(messages.noInstallationsFound) }}
</div>
</template> </template>
</Table> </Table>
<div class="flex justify-end"> <div class="flex justify-end">
@@ -28,8 +40,8 @@
class="!shadow-none !border-surface-4 !border" class="!shadow-none !border-surface-4 !border"
@click="$refs.detectJavaModal.hide()" @click="$refs.detectJavaModal.hide()"
> >
<XIcon /> <XIcon aria-hidden="true" />
Cancel {{ formatMessage(messages.cancel) }}
</button> </button>
</ButtonStyled> </ButtonStyled>
</div> </div>
@@ -38,23 +50,70 @@
</template> </template>
<script setup> <script setup>
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets' import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, Table } from '@modrinth/ui' import {
import { ref } from 'vue' ButtonStyled,
defineMessages,
injectNotificationManager,
Table,
useVIntl,
} from '@modrinth/ui'
import { computed, ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import { find_filtered_jres } from '@/helpers/jre.js' import { find_filtered_jres } from '@/helpers/jre.js'
const { handleError } = injectNotificationManager() const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const messages = defineMessages({
title: {
id: 'app.java-detection.title',
defaultMessage: 'Select Java installation',
},
versionColumn: {
id: 'app.java-detection.columns.version',
defaultMessage: 'Version',
},
pathColumn: {
id: 'app.java-detection.columns.path',
defaultMessage: 'Path',
},
actionsColumn: {
id: 'app.java-detection.columns.actions',
defaultMessage: 'Actions',
},
selected: {
id: 'app.java-detection.selected',
defaultMessage: 'Selected',
},
select: {
id: 'app.java-detection.select',
defaultMessage: 'Select',
},
noInstallationsFound: {
id: 'app.java-detection.no-installations-found',
defaultMessage: 'No Java installations found.',
},
cancel: {
id: 'app.java-detection.cancel',
defaultMessage: 'Cancel',
},
})
const chosenInstallOptions = ref([]) const chosenInstallOptions = ref([])
const detectJavaModal = ref(null) const detectJavaModal = ref(null)
const currentSelected = ref({}) const currentSelected = ref({})
const javaInstallColumns = [ const javaInstallColumns = computed(() => [
{ key: 'version', label: 'Version', width: '9rem' }, { key: 'version', label: formatMessage(messages.versionColumn), width: '9rem' },
{ key: 'path', label: 'Path' }, { key: 'path', label: formatMessage(messages.pathColumn) },
{ key: 'actions', label: 'Actions', align: 'right', width: '10rem' }, {
] key: 'actions',
label: formatMessage(messages.actionsColumn),
align: 'right',
width: '10rem',
},
])
defineExpose({ defineExpose({
show: async (version, currentSelectedJava) => { show: async (version, currentSelectedJava) => {
@@ -6,7 +6,7 @@
autocomplete="off" autocomplete="off"
:disabled="props.disabled" :disabled="props.disabled"
:model-value="props.modelValue ? props.modelValue.path : ''" :model-value="props.modelValue ? props.modelValue.path : ''"
:placeholder="placeholder ?? '/path/to/java'" :placeholder="placeholder ?? formatMessage(messages.pathPlaceholder)"
wrapper-class="installation-input" wrapper-class="installation-input"
@update:model-value=" @update:model-value="
(val) => { (val) => {
@@ -28,6 +28,7 @@
color-fill="text" color-fill="text"
> >
<button <button
:aria-label="formatMessage(messages.testJavaInstallation)"
class="!shadow-none" class="!shadow-none"
:disabled="testingJava || props.disabled" :disabled="testingJava || props.disabled"
@click="runTest(props.modelValue?.path)" @click="runTest(props.modelValue?.path)"
@@ -47,25 +48,31 @@
<span class="installation-buttons"> <span class="installation-buttons">
<ButtonStyled v-if="props.version"> <ButtonStyled v-if="props.version">
<button <button
v-tooltip="testingJavaSuccess === true ? 'Already installed' : undefined" v-tooltip="
testingJavaSuccess === true ? formatMessage(messages.alreadyInstalled) : undefined
"
class="!shadow-none" class="!shadow-none"
:disabled="props.disabled || installingJava || testingJavaSuccess === true" :disabled="props.disabled || installingJava || testingJavaSuccess === true"
@click="reinstallJava" @click="reinstallJava"
> >
<DownloadIcon /> <DownloadIcon />
{{ installingJava ? 'Installing...' : 'Install recommended' }} {{
installingJava
? formatMessage(messages.installing)
: formatMessage(messages.installRecommended)
}}
</button> </button>
</ButtonStyled> </ButtonStyled>
<ButtonStyled> <ButtonStyled>
<button class="!shadow-none" :disabled="props.disabled" @click="autoDetect"> <button class="!shadow-none" :disabled="props.disabled" @click="autoDetect">
<SearchIcon /> <SearchIcon />
Detect {{ formatMessage(messages.detect) }}
</button> </button>
</ButtonStyled> </ButtonStyled>
<ButtonStyled> <ButtonStyled>
<button class="!shadow-none" :disabled="props.disabled" @click="handleJavaFileInput()"> <button class="!shadow-none" :disabled="props.disabled" @click="handleJavaFileInput()">
<FolderSearchIcon /> <FolderSearchIcon />
Browse {{ formatMessage(messages.browse) }}
</button> </button>
</ButtonStyled> </ButtonStyled>
</span> </span>
@@ -82,7 +89,13 @@ import {
SpinnerIcon, SpinnerIcon,
XCircleIcon, XCircleIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, StyledInput } from '@modrinth/ui' import {
ButtonStyled,
defineMessages,
injectNotificationManager,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog' import { open } from '@tauri-apps/plugin-dialog'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
@@ -92,6 +105,38 @@ import { trackEvent } from '@/helpers/analytics'
import { auto_install_java, find_filtered_jres, get_jre } from '@/helpers/jre.js' import { auto_install_java, find_filtered_jres, get_jre } from '@/helpers/jre.js'
const { handleError } = injectNotificationManager() const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const messages = defineMessages({
pathPlaceholder: {
id: 'app.java-selector.path.placeholder',
defaultMessage: '/path/to/java',
},
testJavaInstallation: {
id: 'app.java-selector.test-installation',
defaultMessage: 'Test Java installation',
},
alreadyInstalled: {
id: 'app.java-selector.already-installed',
defaultMessage: 'Already installed',
},
installing: {
id: 'app.java-selector.installing',
defaultMessage: 'Installing...',
},
installRecommended: {
id: 'app.java-selector.install-recommended',
defaultMessage: 'Install recommended',
},
detect: {
id: 'app.java-selector.detect',
defaultMessage: 'Detect',
},
browse: {
id: 'app.java-selector.browse',
defaultMessage: 'Browse',
},
})
const props = defineProps({ const props = defineProps({
id: { id: {
@@ -6,7 +6,7 @@ import {
LanguagesIcon, LanguagesIcon,
ModrinthIcon, ModrinthIcon,
PaintbrushIcon, PaintbrushIcon,
SettingsIcon, Settings2Icon,
ShieldIcon, ShieldIcon,
ToggleRightIcon, ToggleRightIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
@@ -21,19 +21,21 @@ import {
} from '@modrinth/ui' } from '@modrinth/ui'
import { getVersion } from '@tauri-apps/api/app' import { getVersion } from '@tauri-apps/api/app'
import { platform as getOsPlatform, version as getOsVersion } from '@tauri-apps/plugin-os' import { platform as getOsPlatform, version as getOsVersion } from '@tauri-apps/plugin-os'
import { ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue' import PrivacySettings from '@/components/ui/settings/account/PrivacySettings.vue'
import DefaultInstanceSettings from '@/components/ui/settings/DefaultInstanceSettings.vue' import AppearanceSettings from '@/components/ui/settings/display/AppearanceSettings.vue'
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue' import BehaviorSettings from '@/components/ui/settings/display/BehaviorSettings.vue'
import JavaSettings from '@/components/ui/settings/JavaSettings.vue' import FeatureFlagSettings from '@/components/ui/settings/display/FeatureFlagSettings.vue'
import LanguageSettings from '@/components/ui/settings/LanguageSettings.vue' import LanguageSettings from '@/components/ui/settings/display/LanguageSettings.vue'
import PrivacySettings from '@/components/ui/settings/PrivacySettings.vue' import DefaultInstanceSettings from '@/components/ui/settings/instances/DefaultInstanceSettings.vue'
import ResourceManagementSettings from '@/components/ui/settings/ResourceManagementSettings.vue' import JavaSettings from '@/components/ui/settings/instances/JavaSettings.vue'
import ResourceManagementSettings from '@/components/ui/settings/instances/ResourceManagementSettings.vue'
import { get, set } from '@/helpers/settings.ts' import { get, set } from '@/helpers/settings.ts'
import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts' import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts'
import { useTheming } from '@/store/state' import { useTheming } from '@/store/state'
// TODO: Apply COMPONENT_STRUCTURE.md here and extract out common setting option components
const themeStore = useTheming() const themeStore = useTheming()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
@@ -45,64 +47,97 @@ const developerModeEnabled = defineMessage({
defaultMessage: 'Developer mode enabled.', defaultMessage: 'Developer mode enabled.',
}) })
const tabCategories = defineMessages({
display: {
id: 'settings.sidebar.label.display',
defaultMessage: 'Display',
},
account: {
id: 'settings.sidebar.label.account',
defaultMessage: 'Account',
},
instances: {
id: 'app.settings.sidebar.label.instances',
defaultMessage: 'Instances',
},
})
const tabs = [ const tabs = [
{ {
name: defineMessage({ name: defineMessage({
id: 'app.settings.tabs.appearance', id: 'app.settings.tabs.appearance',
defaultMessage: 'Appearance', defaultMessage: 'Appearance',
}), }),
category: tabCategories.display,
icon: PaintbrushIcon, icon: PaintbrushIcon,
content: AppearanceSettings, content: AppearanceSettings,
}, },
{
name: defineMessage({
id: 'app.settings.tabs.behavior',
defaultMessage: 'Behavior',
}),
category: tabCategories.display,
icon: Settings2Icon,
content: BehaviorSettings,
},
{ {
name: defineMessage({ name: defineMessage({
id: 'app.settings.tabs.language', id: 'app.settings.tabs.language',
defaultMessage: 'Language', defaultMessage: 'Language',
}), }),
category: tabCategories.display,
icon: LanguagesIcon, icon: LanguagesIcon,
content: LanguageSettings, content: LanguageSettings,
badge: commonMessages.beta, badge: commonMessages.beta,
}, },
{
name: commonSettingsMessages.featureFlags,
category: tabCategories.display,
icon: ToggleRightIcon,
content: FeatureFlagSettings,
developerOnly: true,
},
{ {
name: defineMessage({ name: defineMessage({
id: 'app.settings.tabs.privacy', id: 'app.settings.tabs.privacy',
defaultMessage: 'Privacy', defaultMessage: 'Privacy',
}), }),
category: tabCategories.account,
icon: ShieldIcon, icon: ShieldIcon,
content: PrivacySettings, content: PrivacySettings,
}, },
{
name: defineMessage({
id: 'app.settings.tabs.java-installations',
defaultMessage: 'Java installations',
}),
icon: CoffeeIcon,
content: JavaSettings,
},
{ {
name: defineMessage({ name: defineMessage({
id: 'app.settings.tabs.default-instance-options', id: 'app.settings.tabs.default-instance-options',
defaultMessage: 'Default instance options', defaultMessage: 'Default instance options',
}), }),
category: tabCategories.instances,
icon: GameIcon, icon: GameIcon,
content: DefaultInstanceSettings, content: DefaultInstanceSettings,
}, },
{
name: defineMessage({
id: 'app.settings.tabs.java-installations',
defaultMessage: 'Java installations',
}),
category: tabCategories.instances,
icon: CoffeeIcon,
content: JavaSettings,
},
{ {
name: defineMessage({ name: defineMessage({
id: 'app.settings.tabs.resource-management', id: 'app.settings.tabs.resource-management',
defaultMessage: 'Resource management', defaultMessage: 'Resource management',
}), }),
category: tabCategories.instances,
icon: GaugeIcon, icon: GaugeIcon,
content: ResourceManagementSettings, content: ResourceManagementSettings,
}, },
{
name: commonSettingsMessages.featureFlags,
icon: ToggleRightIcon,
content: FeatureFlagSettings,
developerOnly: true,
},
] ]
const availableTabs = computed(() => tabs.filter((tab) => !tab.developerOnly || themeStore.devMode))
const modal = ref<InstanceType<typeof TabbedModal> | null>(null) const modal = ref<InstanceType<typeof TabbedModal> | null>(null)
function show() { function show() {
@@ -129,12 +164,15 @@ watch(
function devModeCount() { function devModeCount() {
devModeCounter.value++ devModeCounter.value++
if (devModeCounter.value > 5) { if (devModeCounter.value > 5) {
const selectedTab = modal.value ? availableTabs.value[modal.value.selectedTab] : undefined
themeStore.devMode = !themeStore.devMode themeStore.devMode = !themeStore.devMode
settings.value.developer_mode = !!themeStore.devMode settings.value.developer_mode = !!themeStore.devMode
devModeCounter.value = 0 devModeCounter.value = 0
if (!themeStore.devMode && tabs[modal.value!.selectedTab].developerOnly) { if (modal.value) {
modal.value!.setTab(0) const selectedTabIndex = selectedTab ? availableTabs.value.indexOf(selectedTab) : -1
modal.value.setTab(selectedTabIndex >= 0 ? selectedTabIndex : 0)
} }
} }
} }
@@ -144,13 +182,25 @@ const messages = defineMessages({
id: 'app.settings.downloading', id: 'app.settings.downloading',
defaultMessage: 'Downloading v{version}', defaultMessage: 'Downloading v{version}',
}, },
appVersion: {
id: 'app.settings.app-version',
defaultMessage: 'Modrinth App {version}',
},
macos: {
id: 'app.settings.operating-system.macos',
defaultMessage: 'macOS',
},
developerModeButtonLabel: {
id: 'app.settings.developer-mode-button.label',
defaultMessage: 'Toggle developer mode',
},
}) })
</script> </script>
<template> <template>
<TabbedModal ref="modal" :tabs="tabs.filter((t) => !t.developerOnly || themeStore.devMode)"> <TabbedModal ref="modal" :tabs="availableTabs" :width="'min(928px, calc(95vw - 10rem))'">
<template #title> <template #title>
<span class="flex items-center gap-2 text-lg font-extrabold text-contrast"> <span class="text-2xl font-semibold text-contrast">
<SettingsIcon /> Settings {{ formatMessage(commonMessages.settingsLabel) }}
</span> </span>
</template> </template>
<template #footer> <template #footer>
@@ -168,6 +218,7 @@ const messages = defineMessages({
</p> </p>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button <button
:aria-label="formatMessage(messages.developerModeButtonLabel)"
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation" class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
:class="{ :class="{
'text-brand': themeStore.devMode, 'text-brand': themeStore.devMode,
@@ -175,12 +226,14 @@ const messages = defineMessages({
}" }"
@click="devModeCount" @click="devModeCount"
> >
<ModrinthIcon class="w-6 h-6" /> <ModrinthIcon aria-hidden="true" class="w-6 h-6" />
</button> </button>
<div class="max-w-[200px]"> <div class="max-w-[200px]">
<p class="m-0">Modrinth App {{ version }}</p>
<p class="m-0"> <p class="m-0">
<span v-if="osPlatform === 'macos'">macOS</span> {{ formatMessage(messages.appVersion, { version }) }}
</p>
<p class="m-0">
<span v-if="osPlatform === 'macos'">{{ formatMessage(messages.macos) }}</span>
<span v-else class="capitalize">{{ osPlatform }}</span> <span v-else class="capitalize">{{ osPlatform }}</span>
{{ osVersion }} {{ osVersion }}
</p> </p>
@@ -1,340 +0,0 @@
<script setup lang="ts">
import { Combobox, defineMessages, ThemeSelector, Toggle, useVIntl } from '@modrinth/ui'
import { computed, ref, watch } from 'vue'
import { get, set } from '@/helpers/settings.ts'
import { getOS } from '@/helpers/utils'
import { useTheming } from '@/store/state'
import type { ColorTheme, FeatureFlag } from '@/store/theme.ts'
const themeStore = useTheming()
const { formatMessage } = useVIntl()
const worldsInHomeFlag: FeatureFlag = 'worlds_in_home'
const skipNonEssentialWarningsFlag: FeatureFlag = 'skip_non_essential_warnings'
const skipUnknownPackWarningFlag: FeatureFlag = 'skip_unknown_pack_warning'
const showPlayTimeFlag: FeatureFlag = 'show_instance_play_time'
const messages = defineMessages({
colorThemeTitle: {
id: 'app.appearance-settings.color-theme.title',
defaultMessage: 'Color theme',
},
colorThemeDescription: {
id: 'app.appearance-settings.color-theme.description',
defaultMessage: 'Select your preferred color theme for Modrinth App.',
},
advancedRenderingTitle: {
id: 'app.appearance-settings.advanced-rendering.title',
defaultMessage: 'Advanced rendering',
},
advancedRenderingDescription: {
id: 'app.appearance-settings.advanced-rendering.description',
defaultMessage:
'Enables advanced rendering such as blur effects that may cause performance issues without hardware-accelerated rendering.',
},
hideNametagTitle: {
id: 'app.appearance-settings.hide-nametag.title',
defaultMessage: 'Hide nametag',
},
hideNametagDescription: {
id: 'app.appearance-settings.hide-nametag.description',
defaultMessage: 'Disables the nametag above your player on the skins page.',
},
nativeDecorationsTitle: {
id: 'app.appearance-settings.native-decorations.title',
defaultMessage: 'Native decorations',
},
nativeDecorationsDescription: {
id: 'app.appearance-settings.native-decorations.description',
defaultMessage: 'Use system window frame (app restart required).',
},
minimizeLauncherTitle: {
id: 'app.appearance-settings.minimize-launcher.title',
defaultMessage: 'Minimize launcher',
},
minimizeLauncherDescription: {
id: 'app.appearance-settings.minimize-launcher.description',
defaultMessage: 'Minimize the launcher when a Minecraft process starts.',
},
defaultLandingPageTitle: {
id: 'app.appearance-settings.default-landing-page.title',
defaultMessage: 'Default landing page',
},
defaultLandingPageDescription: {
id: 'app.appearance-settings.default-landing-page.description',
defaultMessage: 'Change the page to which the launcher opens on.',
},
defaultLandingPageHome: {
id: 'app.appearance-settings.default-landing-page.home',
defaultMessage: 'Home',
},
defaultLandingPageLibrary: {
id: 'app.appearance-settings.default-landing-page.library',
defaultMessage: 'Library',
},
selectOption: {
id: 'app.appearance-settings.select-option',
defaultMessage: 'Select an option',
},
jumpBackIntoWorldsTitle: {
id: 'app.appearance-settings.jump-back-into-worlds.title',
defaultMessage: 'Jump back into worlds',
},
jumpBackIntoWorldsDescription: {
id: 'app.appearance-settings.jump-back-into-worlds.description',
defaultMessage: 'Includes recent worlds in the "Jump back in" section on the Home page.',
},
toggleSidebarTitle: {
id: 'app.appearance-settings.toggle-sidebar.title',
defaultMessage: 'Toggle sidebar',
},
toggleSidebarDescription: {
id: 'app.appearance-settings.toggle-sidebar.description',
defaultMessage: 'Enables the ability to toggle the sidebar.',
},
unknownPackWarningTitle: {
id: 'app.appearance-settings.unknown-pack-warning.title',
defaultMessage: 'Warn me before installing unknown modpacks',
},
unknownPackWarningDescription: {
id: 'app.appearance-settings.unknown-pack-warning.description',
defaultMessage:
"If you attempt to install a Modrinth Pack file (.mrpack) that isn't hosted on Modrinth, we'll make sure you understand the risks before installing it.",
},
skipNonEssentialWarningsTitle: {
id: 'app.appearance-settings.skip-non-essential-warnings.title',
defaultMessage: 'Skip non-essential warnings',
},
skipNonEssentialWarningsDescription: {
id: 'app.appearance-settings.skip-non-essential-warnings.description',
defaultMessage:
'Automatically skips low-risk confirmations like duplicate modpack installs, normal content deletion, bulk updates, unlinking modpacks, and repair prompts. Dangerous warnings will still be shown.',
},
showPlayTimeTitle: {
id: 'app.appearance-settings.show-play-time.title',
defaultMessage: 'Show play time',
},
showPlayTimeDescription: {
id: 'app.appearance-settings.show-play-time.description',
defaultMessage: `Displays how much time you've spent playing an instance.`,
},
})
const os = ref(await getOS())
const settings = ref(await get())
const themeOptions = computed(() =>
themeStore
.getThemeOptions()
.filter((theme) => theme !== 'retro' || themeStore.devMode || settings.value.theme === 'retro'),
)
watch(
settings,
async () => {
await set(settings.value)
},
{ deep: true },
)
</script>
<template>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.colorThemeTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.colorThemeDescription) }}</p>
<ThemeSelector
:update-color-theme="
(theme: ColorTheme) => {
themeStore.setThemeState(theme)
settings.theme = theme
}
"
:current-theme="settings.theme"
:theme-options="themeOptions"
system-theme-color="system"
/>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.advancedRenderingTitle) }}
</h2>
<p class="m-0 mt-1">
{{ formatMessage(messages.advancedRenderingDescription) }}
</p>
</div>
<Toggle
id="advanced-rendering"
:model-value="themeStore.advancedRendering"
@update:model-value="
(e) => {
themeStore.advancedRendering = !!e
settings.advanced_rendering = themeStore.advancedRendering
}
"
/>
</div>
<div v-if="os !== 'MacOS'" class="mt-6 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.nativeDecorationsTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.nativeDecorationsDescription) }}</p>
</div>
<Toggle id="native-decorations" v-model="settings.native_decorations" />
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.minimizeLauncherTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.minimizeLauncherDescription) }}</p>
</div>
<Toggle id="minimize-launcher" v-model="settings.hide_on_process_start" />
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.showPlayTimeTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.showPlayTimeDescription) }}</p>
</div>
<Toggle
:model-value="themeStore.getFeatureFlag(showPlayTimeFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(showPlayTimeFlag)
themeStore.featureFlags[showPlayTimeFlag] = newValue
settings.feature_flags[showPlayTimeFlag] = newValue
}
"
/>
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.hideNametagTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.hideNametagDescription) }}</p>
</div>
<Toggle
id="hide-nametag-skins-page"
:model-value="themeStore.hideNametagSkinsPage"
@update:model-value="
(e) => {
themeStore.hideNametagSkinsPage = !!e
settings.hide_nametag_skins_page = themeStore.hideNametagSkinsPage
}
"
/>
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.defaultLandingPageTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.defaultLandingPageDescription) }}</p>
</div>
<Combobox
id="opening-page"
v-model="settings.default_page"
name="Opening page dropdown"
class="max-w-40"
:options="[
{
value: 'Home',
label: formatMessage(messages.defaultLandingPageHome),
},
{
value: 'Library',
label: formatMessage(messages.defaultLandingPageLibrary),
},
]"
:display-value="settings.default_page ?? 'Select an option'"
/>
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.jumpBackIntoWorldsTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.jumpBackIntoWorldsDescription) }}</p>
</div>
<Toggle
:model-value="themeStore.getFeatureFlag(worldsInHomeFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(worldsInHomeFlag)
themeStore.featureFlags[worldsInHomeFlag] = newValue
settings.feature_flags[worldsInHomeFlag] = newValue
}
"
/>
</div>
<div class="mt-6 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.unknownPackWarningTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.unknownPackWarningDescription) }}</p>
</div>
<Toggle
:model-value="!themeStore.getFeatureFlag(skipUnknownPackWarningFlag)"
@update:model-value="
(e) => {
const warnBeforeUnknownPackInstall = !!e
const skipUnknownPackWarning = !warnBeforeUnknownPackInstall
themeStore.featureFlags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
settings.feature_flags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
}
"
/>
</div>
<div class="mt-6 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.skipNonEssentialWarningsTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.skipNonEssentialWarningsDescription) }}</p>
</div>
<Toggle
:model-value="themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)
themeStore.featureFlags[skipNonEssentialWarningsFlag] = newValue
settings.feature_flags[skipNonEssentialWarningsFlag] = newValue
}
"
/>
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.toggleSidebarTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.toggleSidebarDescription) }}</p>
</div>
<Toggle
id="toggle-sidebar"
:model-value="settings.toggle_sidebar"
@update:model-value="
(e) => {
settings.toggle_sidebar = !!e
themeStore.toggleSidebar = settings.toggle_sidebar
}
"
/>
</div>
</template>
@@ -1,186 +0,0 @@
<script setup lang="ts">
import { injectNotificationManager, Slider, StyledInput, Toggle } from '@modrinth/ui'
import { ref, watch } from 'vue'
import useMemorySlider from '@/composables/useMemorySlider'
import { get, set } from '@/helpers/settings.ts'
const { handleError } = injectNotificationManager()
const fetchSettings = await get()
fetchSettings.launchArgs = fetchSettings.extra_launch_args.join(' ')
fetchSettings.envVars = fetchSettings.custom_env_vars.map((x) => x.join('=')).join(' ')
const settings = ref(fetchSettings)
const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) as unknown as {
maxMemory: number
snapPoints: number[]
}
watch(
settings,
async () => {
const setSettings = JSON.parse(JSON.stringify(settings.value))
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
setSettings.custom_env_vars = setSettings.envVars
.trim()
.split(/\s+/)
.filter(Boolean)
.map((x) => x.split('=').filter(Boolean))
if (!setSettings.hooks.pre_launch) {
setSettings.hooks.pre_launch = null
}
if (!setSettings.hooks.wrapper) {
setSettings.hooks.wrapper = null
}
if (!setSettings.hooks.post_exit) {
setSettings.hooks.post_exit = null
}
if (!setSettings.custom_dir) {
setSettings.custom_dir = null
}
await set(setSettings)
},
{ deep: true },
)
</script>
<template>
<div>
<div class="flex flex-col gap-6">
<div class="flex items-center justify-between gap-4">
<div class="flex flex-col gap-1">
<h3 class="m-0 text-lg font-semibold text-contrast">Fullscreen</h3>
<p class="m-0 leading-tight">
Overwrites the options.txt file to start in full screen when launched.
</p>
</div>
<Toggle id="fullscreen" v-model="settings.force_fullscreen" />
</div>
<div class="flex items-center justify-between gap-4">
<div class="flex flex-col gap-1">
<h3 class="m-0 text-lg font-semibold text-contrast">Width</h3>
<p class="m-0 leading-tight">The width of the game window when launched.</p>
</div>
<StyledInput
id="width"
v-model="settings.game_resolution[0]"
:disabled="settings.force_fullscreen"
autocomplete="off"
type="number"
placeholder="Enter width..."
/>
</div>
<div class="flex items-center justify-between gap-4">
<div class="flex flex-col gap-1">
<h3 class="m-0 text-lg font-semibold text-contrast">Height</h3>
<p class="m-0 leading-tight">The height of the game window when launched.</p>
</div>
<StyledInput
id="height"
v-model="settings.game_resolution[1]"
:disabled="settings.force_fullscreen"
autocomplete="off"
type="number"
placeholder="Enter height..."
/>
</div>
</div>
<hr class="my-6 bg-button-border border-none h-[1px]" />
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast">Memory allocated</h2>
<Slider
id="max-memory"
v-model="settings.memory.maximum"
:min="512"
:max="maxMemory"
:step="64"
:snap-points="snapPoints"
:snap-range="512"
unit="MB"
/>
<p class="m-0 mt-1 leading-tight">The memory allocated to each instance when it is ran.</p>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast">Java arguments</h2>
<StyledInput
id="java-args"
v-model="settings.launchArgs"
autocomplete="off"
type="text"
placeholder="Enter java arguments..."
wrapper-class="w-full"
/>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast">Environmental variables</h2>
<StyledInput
id="env-vars"
v-model="settings.envVars"
autocomplete="off"
type="text"
placeholder="Enter environmental variables..."
wrapper-class="w-full"
/>
</div>
</div>
<hr class="my-6 bg-button-border border-none h-[1px]" />
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<h3 class="m-0 text-lg font-semibold text-contrast">Pre launch hook</h3>
<StyledInput
id="pre-launch"
v-model="settings.hooks.pre_launch"
autocomplete="off"
type="text"
placeholder="Enter pre-launch command..."
wrapper-class="w-full"
/>
<p class="m-0 leading-tight">Ran before the instance is launched.</p>
</div>
<div class="flex flex-col gap-2.5">
<h3 class="m-0 text-lg font-semibold text-contrast">Wrapper hook</h3>
<StyledInput
id="wrapper"
v-model="settings.hooks.wrapper"
autocomplete="off"
type="text"
placeholder="Enter wrapper command..."
wrapper-class="w-full"
/>
<p class="m-0 leading-tight">Wrapper command for launching Minecraft.</p>
</div>
<div class="flex flex-col gap-2.5">
<h3 class="m-0 text-lg font-semibold text-contrast">Post exit hook</h3>
<StyledInput
id="post-exit"
v-model="settings.hooks.post_exit"
autocomplete="off"
type="text"
placeholder="Enter post-exit command..."
wrapper-class="w-full"
/>
<p class="m-0 leading-tight">Ran after the game closes.</p>
</div>
</div>
</div>
</template>
@@ -1,212 +0,0 @@
<script setup>
import { BoxIcon, FolderOpenIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
import {
ButtonStyled,
defineMessages,
injectNotificationManager,
Slider,
StyledInput,
Toggle,
useVIntl,
} from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { ref, watch } from 'vue'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import { purge_cache_types } from '@/helpers/cache.js'
import { get, set } from '@/helpers/settings.ts'
import { showAppDbBackupsFolder } from '@/helpers/utils.js'
import { useTheming } from '@/store/state'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const themeStore = useTheming()
const settings = ref(await get())
const purgeCacheConfirmModal = ref(null)
const alwaysShowCopyDetailsFlag = 'always_show_copy_details'
const messages = defineMessages({
alwaysShowCopyDetailsTitle: {
id: 'app.resource-management-settings.always-show-copy-details.title',
defaultMessage: 'Always show copy details',
},
alwaysShowCopyDetailsDescription: {
id: 'app.resource-management-settings.always-show-copy-details.description',
defaultMessage:
'Show the Copy details action while an install is queued or running. It is always available for failed or interrupted installs.',
},
})
watch(
settings,
async () => {
const setSettings = JSON.parse(JSON.stringify(settings.value))
if (!setSettings.custom_dir) {
setSettings.custom_dir = null
}
await set(setSettings)
},
{ deep: true },
)
async function purgeCache() {
await purge_cache_types([
'project',
'project_v3',
'version',
'user',
'team',
'organization',
'file',
'loader_manifest',
'minecraft_manifest',
'categories',
'report_types',
'loaders',
'game_versions',
'donation_platforms',
'file_hash',
'file_update',
'search_results',
'search_results_v3',
]).catch(handleError)
}
function handlePurgeCacheClick() {
if (themeStore.getFeatureFlag('skip_non_essential_warnings')) {
void purgeCache()
return
}
purgeCacheConfirmModal.value?.show()
}
async function openDbBackupsFolder() {
await showAppDbBackupsFolder().catch(handleError)
}
async function findLauncherDir() {
const newDir = await open({
multiple: false,
directory: true,
title: 'Select a new app directory',
})
if (newDir) {
settings.value.custom_dir = newDir
}
}
</script>
<template>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast">App directory</h2>
<StyledInput
id="appDir"
v-model="settings.custom_dir"
:icon="BoxIcon"
type="text"
wrapper-class="w-full"
>
<template #right>
<ButtonStyled circular>
<button class="ml-1.5" @click="findLauncherDir">
<FolderSearchIcon />
</button>
</ButtonStyled>
</template>
</StyledInput>
<p class="m-0 leading-tight text-secondary">
The directory where the launcher stores all of its files. Changes will be applied after
restarting the launcher.
</p>
</div>
<div class="flex flex-col gap-2.5">
<ConfirmModalWrapper
ref="purgeCacheConfirmModal"
title="Are you sure you want to purge the cache?"
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
:has-to-type="false"
proceed-label="Purge cache"
:show-ad-on-close="false"
@proceed="purgeCache"
/>
<h2 class="m-0 text-lg font-semibold text-contrast">App cache</h2>
<button id="purge-cache" class="btn min-w-max" @click="handlePurgeCacheClick">
<TrashIcon />
Purge cache
</button>
<p class="m-0 leading-tight text-secondary">
The Modrinth app stores a cache of data to speed up loading. This can be purged to force the
app to reload data. This may slow down the app temporarily.
</p>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast mt-4">Maximum concurrent downloads</h2>
<Slider
id="max-downloads"
v-model="settings.max_concurrent_downloads"
:min="1"
:max="10"
:step="1"
/>
<p class="m-0 leading-tight text-secondary">
The maximum amount of files the launcher can download at the same time. Set this to a lower
value if you have a poor internet connection. (app restart required to take effect)
</p>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="mt-0 m-0 text-lg font-semibold text-contrast">Maximum concurrent writes</h2>
<Slider
id="max-writes"
v-model="settings.max_concurrent_writes"
:min="1"
:max="50"
:step="1"
/>
<p class="m-0 leading-tight text-secondary">
The maximum amount of files the launcher can write to the disk at once. Set this to a lower
value if you are frequently getting I/O errors. (app restart required to take effect)
</p>
</div>
<div class="flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.alwaysShowCopyDetailsTitle) }}
</h2>
<p class="m-0 mt-1">
{{ formatMessage(messages.alwaysShowCopyDetailsDescription) }}
</p>
</div>
<Toggle
id="always-show-copy-details"
:model-value="themeStore.getFeatureFlag(alwaysShowCopyDetailsFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(alwaysShowCopyDetailsFlag)
themeStore.featureFlags[alwaysShowCopyDetailsFlag] = newValue
settings.feature_flags[alwaysShowCopyDetailsFlag] = newValue
}
"
/>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="mt-0 m-0 text-lg font-semibold text-contrast">App database backups</h2>
<button id="open-db-backups-folder" class="btn min-w-max" @click="openDbBackupsFolder">
<FolderOpenIcon />
Open backups folder
</button>
<p class="m-0 leading-tight text-secondary">
Backups of important app data are stored here in case you need to recover them later.
</p>
</div>
</div>
</template>
@@ -33,6 +33,24 @@ const messages = defineMessages({
id: 'app.ads-consent.manage', id: 'app.ads-consent.manage',
defaultMessage: 'Manage preferences', defaultMessage: 'Manage preferences',
}, },
telemetryTitle: {
id: 'app.settings.privacy.telemetry.title',
defaultMessage: 'Telemetry',
},
telemetryDescription: {
id: 'app.settings.privacy.telemetry.description',
defaultMessage:
'Modrinth collects anonymized analytics and usage data to improve our user experience and customize your experience. By disabling this option, you opt out and your data will no longer be collected.',
},
discordRichPresenceTitle: {
id: 'app.settings.privacy.discord-rich-presence.title',
defaultMessage: 'Discord Rich Presence',
},
discordRichPresenceDescription: {
id: 'app.settings.privacy.discord-rich-presence.description',
defaultMessage:
'Show Modrinth App as your current activity on Discord. This does not affect Rich Presence added to instances by mods. Requires an app restart.',
},
}) })
async function manageAdsPreferences() { async function manageAdsPreferences() {
@@ -59,28 +77,26 @@ watch(
<h2 class="m-0 text-lg font-semibold text-contrast"> <h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.adsConsentTitle) }} {{ formatMessage(messages.adsConsentTitle) }}
</h2> </h2>
<div class="mt-1 flex flex-col gap-2.5 items-start"> <div class="mt-2 flex flex-col gap-2.5 items-start">
<div class="flex flex-col gap-1 items-start">
<div class="text-sm">
{{ formatMessage(messages.adsConsentIntro) }}
</div>
</div>
<ButtonStyled> <ButtonStyled>
<button class="!shadow-none" @click="manageAdsPreferences"> <button class="!shadow-none" @click="manageAdsPreferences">
<Settings2Icon aria-hidden="true" /> <Settings2Icon aria-hidden="true" />
{{ formatMessage(messages.adsConsentManage) }} {{ formatMessage(messages.adsConsentManage) }}
</button> </button>
</ButtonStyled> </ButtonStyled>
<div>
{{ formatMessage(messages.adsConsentIntro) }}
</div>
</div> </div>
</div> </div>
<div class="mt-8 flex items-center justify-between gap-4"> <div class="mt-8 flex items-center justify-between gap-4">
<div> <div>
<h2 class="m-0 text-lg font-semibold text-contrast">Telemetry</h2> <h2 class="m-0 text-lg font-semibold text-contrast">
<p class="m-0 mt-1 text-sm"> {{ formatMessage(messages.telemetryTitle) }}
Modrinth collects anonymized analytics and usage data to improve our user experience and </h2>
customize your experience. By disabling this option, you opt out and your data will no <p class="m-0 mt-1">
longer be collected. {{ formatMessage(messages.telemetryDescription) }}
</p> </p>
</div> </div>
<Toggle id="opt-out-analytics" v-model="settings.telemetry" /> <Toggle id="opt-out-analytics" v-model="settings.telemetry" />
@@ -88,14 +104,11 @@ watch(
<div class="mt-4 flex items-center justify-between gap-4"> <div class="mt-4 flex items-center justify-between gap-4">
<div> <div>
<h2 class="m-0 text-lg font-semibold text-contrast">Discord RPC</h2> <h2 class="m-0 text-lg font-semibold text-contrast">
<p class="m-0 mt-1 text-sm"> {{ formatMessage(messages.discordRichPresenceTitle) }}
Manages the Discord Rich Presence integration. Disabling this will cause 'Modrinth' to no </h2>
longer show up as a game or app you are using on your Discord profile. <p class="m-0 mt-1">
</p> {{ formatMessage(messages.discordRichPresenceDescription) }}
<p class="m-0 mt-2 text-sm">
Note: This will not prevent any instance-specific Discord Rich Presence integrations, such
as those added by mods. (app restart required to take effect)
</p> </p>
</div> </div>
<Toggle id="disable-discord-rpc" v-model="settings.discord_rpc" /> <Toggle id="disable-discord-rpc" v-model="settings.discord_rpc" />
@@ -0,0 +1,108 @@
<script setup lang="ts">
import { defineMessages, ThemeSelector, Toggle, useVIntl } from '@modrinth/ui'
import { computed, ref, watch } from 'vue'
import { get, set } from '@/helpers/settings.ts'
import { getOS } from '@/helpers/utils'
import { useTheming } from '@/store/state'
import type { ColorTheme } from '@/store/theme.ts'
const themeStore = useTheming()
const { formatMessage } = useVIntl()
const messages = defineMessages({
colorThemeTitle: {
id: 'app.appearance-settings.color-theme.title',
defaultMessage: 'Color theme',
},
colorThemeDescription: {
id: 'app.appearance-settings.color-theme.description',
defaultMessage: 'Choose the color theme used by Modrinth App.',
},
advancedRenderingTitle: {
id: 'app.appearance-settings.advanced-rendering.title',
defaultMessage: 'Advanced rendering',
},
advancedRenderingDescription: {
id: 'app.appearance-settings.advanced-rendering.description',
defaultMessage:
'Enable visual effects such as background blur. This may reduce performance without hardware acceleration.',
},
nativeDecorationsTitle: {
id: 'app.appearance-settings.native-decorations.title',
defaultMessage: 'System window frame',
},
nativeDecorationsDescription: {
id: 'app.appearance-settings.native-decorations.description',
defaultMessage:
"Use your operating system's title bar and window controls. Requires an app restart.",
},
})
const os = ref(await getOS())
const settings = ref(await get())
const themeOptions = computed(() =>
themeStore
.getThemeOptions()
.filter((theme) => theme !== 'retro' || themeStore.devMode || settings.value.theme === 'retro'),
)
watch(
settings,
async () => {
await set(settings.value)
},
{ deep: true },
)
</script>
<template>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.colorThemeTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.colorThemeDescription) }}</p>
<ThemeSelector
:update-color-theme="
(theme: ColorTheme) => {
themeStore.setThemeState(theme)
settings.theme = theme
}
"
:current-theme="settings.theme"
:theme-options="themeOptions"
system-theme-color="system"
/>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.advancedRenderingTitle) }}
</h2>
<p class="m-0 mt-1">
{{ formatMessage(messages.advancedRenderingDescription) }}
</p>
</div>
<Toggle
id="advanced-rendering"
:model-value="themeStore.advancedRendering"
@update:model-value="
(e) => {
themeStore.advancedRendering = !!e
settings.advanced_rendering = themeStore.advancedRendering
}
"
/>
</div>
<div v-if="os !== 'MacOS'" class="mt-6 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.nativeDecorationsTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.nativeDecorationsDescription) }}</p>
</div>
<Toggle id="native-decorations" v-model="settings.native_decorations" />
</div>
</template>
@@ -0,0 +1,302 @@
<script setup lang="ts">
import { Chips, defineMessages, Toggle, useVIntl } from '@modrinth/ui'
import { ref, watch } from 'vue'
import { get, set } from '@/helpers/settings.ts'
import { useTheming } from '@/store/state'
import type { FeatureFlag } from '@/store/theme.ts'
const themeStore = useTheming()
const { formatMessage } = useVIntl()
const worldsInHomeFlag: FeatureFlag = 'worlds_in_home'
const skipNonEssentialWarningsFlag: FeatureFlag = 'skip_non_essential_warnings'
const skipUnknownPackWarningFlag: FeatureFlag = 'skip_unknown_pack_warning'
const showPlayTimeFlag: FeatureFlag = 'show_instance_play_time'
type LandingPage = 'Home' | 'Library'
const landingPageOptions: LandingPage[] = ['Home', 'Library']
const messages = defineMessages({
startupAndNavigationTitle: {
id: 'app.behavior-settings.startup-and-navigation.title',
defaultMessage: 'Startup and navigation',
},
contentTitle: {
id: 'app.behavior-settings.content.title',
defaultMessage: 'Home and content',
},
confirmationsTitle: {
id: 'app.behavior-settings.confirmations.title',
defaultMessage: 'Confirmations',
},
minimizeLauncherTitle: {
id: 'app.appearance-settings.minimize-launcher.title',
defaultMessage: 'Minimize app',
},
minimizeLauncherDescription: {
id: 'app.appearance-settings.minimize-launcher.description',
defaultMessage: 'Minimize Modrinth App when Minecraft starts.',
},
defaultLandingPageTitle: {
id: 'app.appearance-settings.default-landing-page.title',
defaultMessage: 'Default landing page',
},
defaultLandingPageDescription: {
id: 'app.appearance-settings.default-landing-page.description',
defaultMessage: 'Choose the page shown when Modrinth App opens.',
},
defaultLandingPageHome: {
id: 'app.appearance-settings.default-landing-page.home',
defaultMessage: 'Home',
},
defaultLandingPageLibrary: {
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.',
},
jumpBackIntoWorldsTitle: {
id: 'app.appearance-settings.jump-back-into-worlds.title',
defaultMessage: 'Jump back into worlds',
},
jumpBackIntoWorldsDescription: {
id: 'app.appearance-settings.jump-back-into-worlds.description',
defaultMessage: 'Show recently played worlds in the "Jump back in" section on the Home page.',
},
showPlayTimeTitle: {
id: 'app.appearance-settings.show-play-time.title',
defaultMessage: 'Show play time',
},
showPlayTimeDescription: {
id: 'app.appearance-settings.show-play-time.description',
defaultMessage: `Show how long you've played each instance.`,
},
hideNametagTitle: {
id: 'app.appearance-settings.hide-nametag.title',
defaultMessage: 'Hide nametag',
},
hideNametagDescription: {
id: 'app.appearance-settings.hide-nametag.description',
defaultMessage: 'Hide your username above the player preview on the Skin selector page.',
},
unknownPackWarningTitle: {
id: 'app.appearance-settings.unknown-pack-warning.title',
defaultMessage: 'Warn me before installing unknown modpacks',
},
unknownPackWarningDescription: {
id: 'app.appearance-settings.unknown-pack-warning.description',
defaultMessage:
"Show a safety warning before installing a Modrinth Pack (.mrpack) that isn't hosted on Modrinth.",
},
skipNonEssentialWarningsTitle: {
id: 'app.appearance-settings.skip-non-essential-warnings.title',
defaultMessage: 'Skip non-essential warnings',
},
skipNonEssentialWarningsDescription: {
id: 'app.appearance-settings.skip-non-essential-warnings.description',
defaultMessage:
'Skip confirmations for low-risk actions such as duplicate installs, normal content deletion, bulk updates, unlinking, and repairs. Warnings for dangerous actions are always shown.',
},
})
function formatLandingPageLabel(page: LandingPage) {
switch (page) {
case 'Home':
return formatMessage(messages.defaultLandingPageHome)
case 'Library':
return formatMessage(messages.defaultLandingPageLibrary)
}
}
const settings = ref(await get())
watch(
settings,
async () => {
await set(settings.value)
},
{ deep: true },
)
</script>
<template>
<section>
<h2 class="m-0 text-xl font-semibold text-contrast">
{{ formatMessage(messages.startupAndNavigationTitle) }}
</h2>
<div class="mt-4 flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.defaultLandingPageTitle) }}
</h3>
<Chips
v-model="settings.default_page"
:items="landingPageOptions"
:format-label="formatLandingPageLabel"
:capitalize="false"
:aria-label="formatMessage(messages.defaultLandingPageTitle)"
/>
<p class="m-0">
{{ formatMessage(messages.defaultLandingPageDescription) }}
</p>
</div>
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.minimizeLauncherTitle) }}
</h3>
<p class="m-0 mt-1">
{{ formatMessage(messages.minimizeLauncherDescription) }}
</p>
</div>
<Toggle id="minimize-launcher" v-model="settings.hide_on_process_start" />
</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"
:model-value="settings.toggle_sidebar"
@update:model-value="
(e) => {
settings.toggle_sidebar = !!e
themeStore.toggleSidebar = settings.toggle_sidebar
}
"
/>
</div>
</div>
</section>
<section class="mt-8 border-0 border-t border-solid border-divider pt-6">
<h2 class="m-0 text-xl font-semibold text-contrast">
{{ formatMessage(messages.contentTitle) }}
</h2>
<div class="mt-4 flex flex-col gap-6">
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.jumpBackIntoWorldsTitle) }}
</h3>
<p class="m-0 mt-1">
{{ formatMessage(messages.jumpBackIntoWorldsDescription) }}
</p>
</div>
<Toggle
id="jump-back-into-worlds"
:model-value="themeStore.getFeatureFlag(worldsInHomeFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(worldsInHomeFlag)
themeStore.featureFlags[worldsInHomeFlag] = newValue
settings.feature_flags[worldsInHomeFlag] = newValue
}
"
/>
</div>
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.showPlayTimeTitle) }}
</h3>
<p class="m-0 mt-1">{{ formatMessage(messages.showPlayTimeDescription) }}</p>
</div>
<Toggle
id="show-play-time"
:model-value="themeStore.getFeatureFlag(showPlayTimeFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(showPlayTimeFlag)
themeStore.featureFlags[showPlayTimeFlag] = newValue
settings.feature_flags[showPlayTimeFlag] = newValue
}
"
/>
</div>
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.hideNametagTitle) }}
</h3>
<p class="m-0 mt-1">{{ formatMessage(messages.hideNametagDescription) }}</p>
</div>
<Toggle
id="hide-nametag-skins-page"
:model-value="themeStore.hideNametagSkinsPage"
@update:model-value="
(e) => {
themeStore.hideNametagSkinsPage = !!e
settings.hide_nametag_skins_page = themeStore.hideNametagSkinsPage
}
"
/>
</div>
</div>
</section>
<section class="mt-8 border-0 border-t border-solid border-divider pt-6">
<h2 class="m-0 text-xl font-semibold text-contrast">
{{ formatMessage(messages.confirmationsTitle) }}
</h2>
<div class="mt-4 flex flex-col gap-6">
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.unknownPackWarningTitle) }}
</h3>
<p class="m-0 mt-1">
{{ formatMessage(messages.unknownPackWarningDescription) }}
</p>
</div>
<Toggle
id="warn-before-installing-unknown-modpacks"
:model-value="!themeStore.getFeatureFlag(skipUnknownPackWarningFlag)"
@update:model-value="
(e) => {
const warnBeforeUnknownPackInstall = !!e
const skipUnknownPackWarning = !warnBeforeUnknownPackInstall
themeStore.featureFlags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
settings.feature_flags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
}
"
/>
</div>
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.skipNonEssentialWarningsTitle) }}
</h3>
<p class="m-0 mt-1">
{{ formatMessage(messages.skipNonEssentialWarningsDescription) }}
</p>
</div>
<Toggle
id="skip-non-essential-warnings"
:model-value="themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)
themeStore.featureFlags[skipNonEssentialWarningsFlag] = newValue
settings.feature_flags[skipNonEssentialWarningsFlag] = newValue
}
"
/>
</div>
</div>
</section>
</template>
@@ -2,6 +2,7 @@
import { import {
Admonition, Admonition,
AutoLink, AutoLink,
commonSettingsMessages,
IntlFormatted, IntlFormatted,
LanguageSelector, LanguageSelector,
languageSelectorMessages, languageSelectorMessages,
@@ -43,7 +44,9 @@ async function onLocaleChange(newLocale: string) {
</script> </script>
<template> <template>
<h2 class="m-0 text-lg font-semibold text-contrast">Language</h2> <h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(commonSettingsMessages.language) }}
</h2>
<Admonition type="warning" class="mt-2 mb-4"> <Admonition type="warning" class="mt-2 mb-4">
{{ formatMessage(languageSelectorMessages.languageWarning, { platform }) }} {{ formatMessage(languageSelectorMessages.languageWarning, { platform }) }}
@@ -0,0 +1,333 @@
<script setup lang="ts">
import {
defineMessages,
injectNotificationManager,
Slider,
StyledInput,
Toggle,
useVIntl,
} from '@modrinth/ui'
import { ref, watch } from 'vue'
import useMemorySlider from '@/composables/useMemorySlider'
import { get, set } from '@/helpers/settings.ts'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const messages = defineMessages({
fullscreenTitle: {
id: 'app.settings.default-instance-options.fullscreen.title',
defaultMessage: 'Fullscreen',
},
fullscreenDescription: {
id: 'app.settings.default-instance-options.fullscreen.description',
defaultMessage: 'Start instances in fullscreen by updating their options.txt file.',
},
widthTitle: {
id: 'app.settings.default-instance-options.width.title',
defaultMessage: 'Width',
},
widthDescription: {
id: 'app.settings.default-instance-options.width.description',
defaultMessage: 'The width of the game window when launched.',
},
widthPlaceholder: {
id: 'app.settings.default-instance-options.width.placeholder',
defaultMessage: 'Enter width...',
},
heightTitle: {
id: 'app.settings.default-instance-options.height.title',
defaultMessage: 'Height',
},
heightDescription: {
id: 'app.settings.default-instance-options.height.description',
defaultMessage: 'The height of the game window when launched.',
},
heightPlaceholder: {
id: 'app.settings.default-instance-options.height.placeholder',
defaultMessage: 'Enter height...',
},
memoryAllocationTitle: {
id: 'app.settings.default-instance-options.memory-allocation.title',
defaultMessage: 'Memory allocation',
},
memoryAllocationDescription: {
id: 'app.settings.default-instance-options.memory-allocation.description',
defaultMessage: 'Maximum memory available to each instance.',
},
javaArgumentsTitle: {
id: 'app.settings.default-instance-options.java-arguments.title',
defaultMessage: 'Java arguments',
},
javaArgumentsPlaceholder: {
id: 'app.settings.default-instance-options.java-arguments.placeholder',
defaultMessage: 'Enter Java arguments...',
},
javaArgumentsDescription: {
id: 'app.settings.default-instance-options.java-arguments.description',
defaultMessage: 'Arguments passed to Java when launching an instance.',
},
environmentVariablesTitle: {
id: 'app.settings.default-instance-options.environment-variables.title',
defaultMessage: 'Environment variables',
},
environmentVariablesPlaceholder: {
id: 'app.settings.default-instance-options.environment-variables.placeholder',
defaultMessage: 'Enter environment variables...',
},
environmentVariablesDescription: {
id: 'app.settings.default-instance-options.environment-variables.description',
defaultMessage: 'Environment variables set when launching an instance.',
},
preLaunchHookTitle: {
id: 'app.settings.default-instance-options.pre-launch-hook.title',
defaultMessage: 'Pre-launch hook',
},
preLaunchHookPlaceholder: {
id: 'app.settings.default-instance-options.pre-launch-hook.placeholder',
defaultMessage: 'Enter pre-launch command...',
},
preLaunchHookDescription: {
id: 'app.settings.default-instance-options.pre-launch-hook.description',
defaultMessage: 'Runs before the instance starts.',
},
wrapperHookTitle: {
id: 'app.settings.default-instance-options.wrapper-hook.title',
defaultMessage: 'Wrapper hook',
},
wrapperHookPlaceholder: {
id: 'app.settings.default-instance-options.wrapper-hook.placeholder',
defaultMessage: 'Enter wrapper command...',
},
wrapperHookDescription: {
id: 'app.settings.default-instance-options.wrapper-hook.description',
defaultMessage: 'Command used to wrap the Minecraft launch process.',
},
postExitHookTitle: {
id: 'app.settings.default-instance-options.post-exit-hook.title',
defaultMessage: 'Post-exit hook',
},
postExitHookPlaceholder: {
id: 'app.settings.default-instance-options.post-exit-hook.placeholder',
defaultMessage: 'Enter post-exit command...',
},
postExitHookDescription: {
id: 'app.settings.default-instance-options.post-exit-hook.description',
defaultMessage: 'Runs after the game closes.',
},
})
const fetchSettings = await get()
fetchSettings.launchArgs = fetchSettings.extra_launch_args.join(' ')
fetchSettings.envVars = fetchSettings.custom_env_vars.map((x) => x.join('=')).join(' ')
const settings = ref(fetchSettings)
const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) as unknown as {
maxMemory: number
snapPoints: number[]
}
watch(
settings,
async () => {
const setSettings = JSON.parse(JSON.stringify(settings.value))
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
setSettings.custom_env_vars = setSettings.envVars
.trim()
.split(/\s+/)
.filter(Boolean)
.map((x) => x.split('=').filter(Boolean))
if (!setSettings.hooks.pre_launch) {
setSettings.hooks.pre_launch = null
}
if (!setSettings.hooks.wrapper) {
setSettings.hooks.wrapper = null
}
if (!setSettings.hooks.post_exit) {
setSettings.hooks.post_exit = null
}
if (!setSettings.custom_dir) {
setSettings.custom_dir = null
}
await set(setSettings)
},
{ deep: true },
)
</script>
<template>
<div>
<div class="flex flex-col gap-6">
<div class="flex items-center justify-between gap-4">
<div class="flex flex-col gap-1">
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.fullscreenTitle) }}
</h3>
<p class="m-0 leading-tight">
{{ formatMessage(messages.fullscreenDescription) }}
</p>
</div>
<Toggle id="fullscreen" v-model="settings.force_fullscreen" />
</div>
<div class="flex items-center justify-between gap-4">
<div class="flex flex-col gap-1">
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.widthTitle) }}
</h3>
<p class="m-0 leading-tight">
{{ formatMessage(messages.widthDescription) }}
</p>
</div>
<StyledInput
id="width"
v-model="settings.game_resolution[0]"
:disabled="settings.force_fullscreen"
autocomplete="off"
type="number"
:placeholder="formatMessage(messages.widthPlaceholder)"
/>
</div>
<div class="flex items-center justify-between gap-4">
<div class="flex flex-col gap-1">
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.heightTitle) }}
</h3>
<p class="m-0 leading-tight">
{{ formatMessage(messages.heightDescription) }}
</p>
</div>
<StyledInput
id="height"
v-model="settings.game_resolution[1]"
:disabled="settings.force_fullscreen"
autocomplete="off"
type="number"
:placeholder="formatMessage(messages.heightPlaceholder)"
/>
</div>
</div>
<hr class="my-6 bg-button-border border-none h-[1px]" />
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.memoryAllocationTitle) }}
</h2>
<Slider
id="max-memory"
v-model="settings.memory.maximum"
:min="512"
:max="maxMemory"
:step="64"
:snap-points="snapPoints"
:snap-range="512"
unit="MB"
/>
<p class="m-0 mt-1 leading-tight">
{{ formatMessage(messages.memoryAllocationDescription) }}
</p>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.javaArgumentsTitle) }}
</h2>
<StyledInput
id="java-args"
v-model="settings.launchArgs"
autocomplete="off"
type="text"
:placeholder="formatMessage(messages.javaArgumentsPlaceholder)"
wrapper-class="w-full"
/>
<p class="m-0 leading-tight">
{{ formatMessage(messages.javaArgumentsDescription) }}
</p>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.environmentVariablesTitle) }}
</h2>
<StyledInput
id="env-vars"
v-model="settings.envVars"
autocomplete="off"
type="text"
:placeholder="formatMessage(messages.environmentVariablesPlaceholder)"
wrapper-class="w-full"
/>
<p class="m-0 leading-tight">
{{ formatMessage(messages.environmentVariablesDescription) }}
</p>
</div>
</div>
<hr class="my-6 bg-button-border border-none h-[1px]" />
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.preLaunchHookTitle) }}
</h3>
<StyledInput
id="pre-launch"
v-model="settings.hooks.pre_launch"
autocomplete="off"
type="text"
:placeholder="formatMessage(messages.preLaunchHookPlaceholder)"
wrapper-class="w-full"
/>
<p class="m-0 leading-tight">
{{ formatMessage(messages.preLaunchHookDescription) }}
</p>
</div>
<div class="flex flex-col gap-2.5">
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.wrapperHookTitle) }}
</h3>
<StyledInput
id="wrapper"
v-model="settings.hooks.wrapper"
autocomplete="off"
type="text"
:placeholder="formatMessage(messages.wrapperHookPlaceholder)"
wrapper-class="w-full"
/>
<p class="m-0 leading-tight">
{{ formatMessage(messages.wrapperHookDescription) }}
</p>
</div>
<div class="flex flex-col gap-2.5">
<h3 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.postExitHookTitle) }}
</h3>
<StyledInput
id="post-exit"
v-model="settings.hooks.post_exit"
autocomplete="off"
type="text"
:placeholder="formatMessage(messages.postExitHookPlaceholder)"
wrapper-class="w-full"
/>
<p class="m-0 leading-tight">
{{ formatMessage(messages.postExitHookDescription) }}
</p>
</div>
</div>
</div>
</template>
@@ -1,11 +1,19 @@
<script setup> <script setup>
import { injectNotificationManager } from '@modrinth/ui' import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
import { ref } from 'vue' import { ref } from 'vue'
import JavaSelector from '@/components/ui/JavaSelector.vue' import JavaSelector from '@/components/ui/JavaSelector.vue'
import { get_java_versions, set_java_version } from '@/helpers/jre' import { get_java_versions, set_java_version } from '@/helpers/jre'
const { handleError } = injectNotificationManager() const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const messages = defineMessages({
javaLocation: {
id: 'app.settings.java-installations.location.title',
defaultMessage: 'Java {version, number} location',
},
})
const javaVersions = ref(await get_java_versions().catch(handleError)) const javaVersions = ref(await get_java_versions().catch(handleError))
async function updateJavaVersion(version) { async function updateJavaVersion(version) {
@@ -28,7 +36,7 @@ async function updateJavaVersion(version) {
class="flex flex-col gap-2.5" class="flex flex-col gap-2.5"
> >
<h2 class="m-0 text-lg font-semibold text-contrast" :class="{ 'mt-4': index !== 0 }"> <h2 class="m-0 text-lg font-semibold text-contrast" :class="{ 'mt-4': index !== 0 }">
Java {{ javaVersion }} location {{ formatMessage(messages.javaLocation, { version: javaVersion }) }}
</h2> </h2>
<JavaSelector <JavaSelector
:id="'java-selector-' + javaVersion" :id="'java-selector-' + javaVersion"
@@ -0,0 +1,292 @@
<script setup>
import { BoxIcon, FolderOpenIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
import {
ButtonStyled,
defineMessages,
injectNotificationManager,
Slider,
StyledInput,
Toggle,
useVIntl,
} from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { ref, watch } from 'vue'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
import { purge_cache_types } from '@/helpers/cache.js'
import { get, set } from '@/helpers/settings.ts'
import { showAppDbBackupsFolder } from '@/helpers/utils.js'
import { useTheming } from '@/store/state'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const themeStore = useTheming()
const settings = ref(await get())
const purgeCacheConfirmModal = ref(null)
const alwaysShowCopyDetailsFlag = 'always_show_copy_details'
const messages = defineMessages({
appDirectoryTitle: {
id: 'app.settings.resource-management.app-directory.title',
defaultMessage: 'App directory',
},
appDirectoryDescription: {
id: 'app.settings.resource-management.app-directory.description',
defaultMessage:
'Where Modrinth App stores instances and other files. Changes take effect after restarting the app.',
},
selectAppDirectory: {
id: 'app.settings.resource-management.app-directory.select',
defaultMessage: 'Select a new app directory',
},
browseAppDirectory: {
id: 'app.settings.resource-management.app-directory.browse',
defaultMessage: 'Browse for an app directory',
},
appCacheTitle: {
id: 'app.settings.resource-management.app-cache.title',
defaultMessage: 'App cache',
},
purgeCache: {
id: 'app.settings.resource-management.app-cache.purge',
defaultMessage: 'Purge cache',
},
purgeCacheConfirmTitle: {
id: 'app.settings.resource-management.app-cache.confirm.title',
defaultMessage: 'Purge the app cache?',
},
purgeCacheConfirmDescription: {
id: 'app.settings.resource-management.app-cache.confirm.description',
defaultMessage: 'The app may load more slowly until the cache is rebuilt.',
},
appCacheDescription: {
id: 'app.settings.resource-management.app-cache.description',
defaultMessage:
'Clear cached data and download it again from Modrinth. The app may load more slowly until the cache is rebuilt.',
},
maximumConcurrentDownloadsTitle: {
id: 'app.settings.resource-management.maximum-concurrent-downloads.title',
defaultMessage: 'Maximum concurrent downloads',
},
maximumConcurrentDownloadsDescription: {
id: 'app.settings.resource-management.maximum-concurrent-downloads.description',
defaultMessage:
'Number of files the app can download at once. Lower this if downloads are unreliable on your connection. Requires an app restart.',
},
maximumConcurrentWritesTitle: {
id: 'app.settings.resource-management.maximum-concurrent-writes.title',
defaultMessage: 'Maximum concurrent writes',
},
maximumConcurrentWritesDescription: {
id: 'app.settings.resource-management.maximum-concurrent-writes.description',
defaultMessage:
'Number of files the app can write to disk at once. Lower this if you frequently encounter I/O errors. Requires an app restart.',
},
alwaysShowCopyDetailsTitle: {
id: 'app.settings.resource-management.always-show-copy-details.title',
defaultMessage: 'Always show copy details',
},
alwaysShowCopyDetailsDescription: {
id: 'app.settings.resource-management.always-show-copy-details.description',
defaultMessage:
'Show the Copy details action while an install is queued or running. It is always available for failed or interrupted installs.',
},
appDatabaseBackupsTitle: {
id: 'app.settings.resource-management.app-database-backups.title',
defaultMessage: 'App database backups',
},
openBackupsFolder: {
id: 'app.settings.resource-management.app-database-backups.open-folder',
defaultMessage: 'Open backups folder',
},
appDatabaseBackupsDescription: {
id: 'app.settings.resource-management.app-database-backups.description',
defaultMessage:
'Backups of important app data are stored here in case you need to recover them later.',
},
})
watch(
settings,
async () => {
const setSettings = JSON.parse(JSON.stringify(settings.value))
if (!setSettings.custom_dir) {
setSettings.custom_dir = null
}
await set(setSettings)
},
{ deep: true },
)
async function purgeCache() {
await purge_cache_types([
'project',
'project_v3',
'version',
'user',
'team',
'organization',
'file',
'loader_manifest',
'minecraft_manifest',
'categories',
'report_types',
'loaders',
'game_versions',
'donation_platforms',
'file_hash',
'file_update',
'search_results',
'search_results_v3',
]).catch(handleError)
}
function handlePurgeCacheClick() {
if (themeStore.getFeatureFlag('skip_non_essential_warnings')) {
void purgeCache()
return
}
purgeCacheConfirmModal.value?.show()
}
async function openDbBackupsFolder() {
await showAppDbBackupsFolder().catch(handleError)
}
async function findLauncherDir() {
const newDir = await open({
multiple: false,
directory: true,
title: formatMessage(messages.selectAppDirectory),
})
if (newDir) {
settings.value.custom_dir = newDir
}
}
</script>
<template>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.appDirectoryTitle) }}
</h2>
<StyledInput
id="appDir"
v-model="settings.custom_dir"
:icon="BoxIcon"
type="text"
wrapper-class="w-full"
>
<template #right>
<ButtonStyled circular>
<button
v-tooltip="formatMessage(messages.browseAppDirectory)"
:aria-label="formatMessage(messages.browseAppDirectory)"
class="ml-1.5"
@click="findLauncherDir"
>
<FolderSearchIcon aria-hidden="true" />
</button>
</ButtonStyled>
</template>
</StyledInput>
<p class="m-0 leading-tight text-secondary">
{{ formatMessage(messages.appDirectoryDescription) }}
</p>
</div>
<div class="flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.alwaysShowCopyDetailsTitle) }}
</h2>
<p class="m-0 mt-1">
{{ formatMessage(messages.alwaysShowCopyDetailsDescription) }}
</p>
</div>
<Toggle
id="always-show-copy-details"
:model-value="themeStore.getFeatureFlag(alwaysShowCopyDetailsFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(alwaysShowCopyDetailsFlag)
themeStore.featureFlags[alwaysShowCopyDetailsFlag] = newValue
settings.feature_flags[alwaysShowCopyDetailsFlag] = newValue
}
"
/>
</div>
<div class="flex flex-col gap-2.5">
<ConfirmModalWrapper
ref="purgeCacheConfirmModal"
:title="formatMessage(messages.purgeCacheConfirmTitle)"
:description="formatMessage(messages.purgeCacheConfirmDescription)"
:has-to-type="false"
:proceed-label="formatMessage(messages.purgeCache)"
:show-ad-on-close="false"
@proceed="purgeCache"
/>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.appCacheTitle) }}
</h2>
<button id="purge-cache" class="btn min-w-max" @click="handlePurgeCacheClick">
<TrashIcon aria-hidden="true" />
{{ formatMessage(messages.purgeCache) }}
</button>
<p class="m-0 leading-tight text-secondary">
{{ formatMessage(messages.appCacheDescription) }}
</p>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="m-0 text-lg font-semibold text-contrast mt-4">
{{ formatMessage(messages.maximumConcurrentDownloadsTitle) }}
</h2>
<Slider
id="max-downloads"
v-model="settings.max_concurrent_downloads"
:min="1"
:max="10"
:step="1"
/>
<p class="m-0 leading-tight text-secondary">
{{ formatMessage(messages.maximumConcurrentDownloadsDescription) }}
</p>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="mt-0 m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.maximumConcurrentWritesTitle) }}
</h2>
<Slider
id="max-writes"
v-model="settings.max_concurrent_writes"
:min="1"
:max="50"
:step="1"
/>
<p class="m-0 leading-tight text-secondary">
{{ formatMessage(messages.maximumConcurrentWritesDescription) }}
</p>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="mt-0 m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.appDatabaseBackupsTitle) }}
</h2>
<button id="open-db-backups-folder" class="btn min-w-max" @click="openDbBackupsFolder">
<FolderOpenIcon aria-hidden="true" />
{{ formatMessage(messages.openBackupsFolder) }}
</button>
<p class="m-0 leading-tight text-secondary">
{{ formatMessage(messages.appDatabaseBackupsDescription) }}
</p>
</div>
</div>
</template>
+1 -1
View File
@@ -37,7 +37,7 @@ export type AppSettings = {
theme: ColorTheme theme: ColorTheme
locale: string locale: string
default_page: 'home' | 'library' default_page: 'Home' | 'Library'
collapsed_navigation: boolean collapsed_navigation: boolean
hide_nametag_skins_page: boolean hide_nametag_skins_page: boolean
advanced_rendering: boolean advanced_rendering: boolean
@@ -182,9 +182,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "الزخارف التقليدية" "message": "الزخارف التقليدية"
}, },
"app.appearance-settings.select-option": {
"message": "حدد خياراً"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "يعرض مقدار الوقت الذي قضيته في لعب إحدى المهام." "message": "يعرض مقدار الوقت الذي قضيته في لعب إحدى المهام."
}, },
@@ -113,9 +113,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Systémové dekorace" "message": "Systémové dekorace"
}, },
"app.appearance-settings.select-option": {
"message": "Vyberte možnost"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Zobrazuje, kolik času jsi strávil hraním instance." "message": "Zobrazuje, kolik času jsi strávil hraním instance."
}, },
@@ -173,9 +173,6 @@
"app.appearance-settings.minimize-launcher.title": { "app.appearance-settings.minimize-launcher.title": {
"message": "Minimer launcher" "message": "Minimer launcher"
}, },
"app.appearance-settings.select-option": {
"message": "Vælg en mulighed"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Vis hvor meget tid du har brugt på at spille en instance." "message": "Vis hvor meget tid du har brugt på at spille en instance."
}, },
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Native Dekorationen" "message": "Native Dekorationen"
}, },
"app.appearance-settings.select-option": {
"message": "Option wählen"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Zeigt an, wie viel Zeit du mit dem Spielen einer Instanz verbracht hast." "message": "Zeigt an, wie viel Zeit du mit dem Spielen einer Instanz verbracht hast."
}, },
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Native Dekorationen" "message": "Native Dekorationen"
}, },
"app.appearance-settings.select-option": {
"message": "Wähle eine Option"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Zeigt an, wie viel Zeit du damit verbracht hast, eine Instanz zu spielen." "message": "Zeigt an, wie viel Zeit du damit verbracht hast, eine Instanz zu spielen."
}, },
+231 -21
View File
@@ -144,19 +144,19 @@
"message": "Your privacy and how ads support Modrinth" "message": "Your privacy and how ads support Modrinth"
}, },
"app.appearance-settings.advanced-rendering.description": { "app.appearance-settings.advanced-rendering.description": {
"message": "Enables advanced rendering such as blur effects that may cause performance issues without hardware-accelerated rendering." "message": "Enable visual effects such as background blur. This may reduce performance without hardware acceleration."
}, },
"app.appearance-settings.advanced-rendering.title": { "app.appearance-settings.advanced-rendering.title": {
"message": "Advanced rendering" "message": "Advanced rendering"
}, },
"app.appearance-settings.color-theme.description": { "app.appearance-settings.color-theme.description": {
"message": "Select your preferred color theme for Modrinth App." "message": "Choose the color theme used by Modrinth App."
}, },
"app.appearance-settings.color-theme.title": { "app.appearance-settings.color-theme.title": {
"message": "Color theme" "message": "Color theme"
}, },
"app.appearance-settings.default-landing-page.description": { "app.appearance-settings.default-landing-page.description": {
"message": "Change the page to which the launcher opens on." "message": "Choose the page shown when Modrinth App opens."
}, },
"app.appearance-settings.default-landing-page.home": { "app.appearance-settings.default-landing-page.home": {
"message": "Home" "message": "Home"
@@ -168,52 +168,49 @@
"message": "Default landing page" "message": "Default landing page"
}, },
"app.appearance-settings.hide-nametag.description": { "app.appearance-settings.hide-nametag.description": {
"message": "Disables the nametag above your player on the skins page." "message": "Hide your username above the player preview on the Skin selector page."
}, },
"app.appearance-settings.hide-nametag.title": { "app.appearance-settings.hide-nametag.title": {
"message": "Hide nametag" "message": "Hide nametag"
}, },
"app.appearance-settings.jump-back-into-worlds.description": { "app.appearance-settings.jump-back-into-worlds.description": {
"message": "Includes recent worlds in the \"Jump back in\" section on the Home page." "message": "Show recently played worlds in the \"Jump back in\" section on the Home page."
}, },
"app.appearance-settings.jump-back-into-worlds.title": { "app.appearance-settings.jump-back-into-worlds.title": {
"message": "Jump back into worlds" "message": "Jump back into worlds"
}, },
"app.appearance-settings.minimize-launcher.description": { "app.appearance-settings.minimize-launcher.description": {
"message": "Minimize the launcher when a Minecraft process starts." "message": "Minimize Modrinth App when Minecraft starts."
}, },
"app.appearance-settings.minimize-launcher.title": { "app.appearance-settings.minimize-launcher.title": {
"message": "Minimize launcher" "message": "Minimize app"
}, },
"app.appearance-settings.native-decorations.description": { "app.appearance-settings.native-decorations.description": {
"message": "Use system window frame (app restart required)." "message": "Use your operating system's title bar and window controls. Requires an app restart."
}, },
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Native decorations" "message": "System window frame"
},
"app.appearance-settings.select-option": {
"message": "Select an option"
}, },
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Displays how much time you've spent playing an instance." "message": "Show how long you've played each instance."
}, },
"app.appearance-settings.show-play-time.title": { "app.appearance-settings.show-play-time.title": {
"message": "Show play time" "message": "Show play time"
}, },
"app.appearance-settings.skip-non-essential-warnings.description": { "app.appearance-settings.skip-non-essential-warnings.description": {
"message": "Automatically skips low-risk confirmations like duplicate modpack installs, normal content deletion, bulk updates, unlinking modpacks, and repair prompts. Dangerous warnings will still be shown." "message": "Skip confirmations for low-risk actions such as duplicate installs, normal content deletion, bulk updates, unlinking, and repairs. Warnings for dangerous actions are always shown."
}, },
"app.appearance-settings.skip-non-essential-warnings.title": { "app.appearance-settings.skip-non-essential-warnings.title": {
"message": "Skip non-essential warnings" "message": "Skip non-essential warnings"
}, },
"app.appearance-settings.toggle-sidebar.description": { "app.appearance-settings.toggle-sidebar.description": {
"message": "Enables the ability to toggle the sidebar." "message": "Hide the right sidebar by default and add a button to show or hide it."
}, },
"app.appearance-settings.toggle-sidebar.title": { "app.appearance-settings.toggle-sidebar.title": {
"message": "Toggle sidebar" "message": "Hide right sidebar"
}, },
"app.appearance-settings.unknown-pack-warning.description": { "app.appearance-settings.unknown-pack-warning.description": {
"message": "If you attempt to install a Modrinth Pack file (.mrpack) that isn't hosted on Modrinth, we'll make sure you understand the risks before installing it." "message": "Show a safety warning before installing a Modrinth Pack (.mrpack) that isn't hosted on Modrinth."
}, },
"app.appearance-settings.unknown-pack-warning.title": { "app.appearance-settings.unknown-pack-warning.title": {
"message": "Warn me before installing unknown modpacks" "message": "Warn me before installing unknown modpacks"
@@ -224,6 +221,15 @@
"app.auth-servers.unreachable.header": { "app.auth-servers.unreachable.header": {
"message": "Cannot reach authentication servers" "message": "Cannot reach authentication servers"
}, },
"app.behavior-settings.confirmations.title": {
"message": "Confirmations"
},
"app.behavior-settings.content.title": {
"message": "Home and content"
},
"app.behavior-settings.startup-and-navigation.title": {
"message": "Startup and navigation"
},
"app.browse.add-servers-to-instance": { "app.browse.add-servers-to-instance": {
"message": "Adding server to instance" "message": "Adding server to instance"
}, },
@@ -572,6 +578,51 @@
"app.instance.worlds.search-worlds-placeholder": { "app.instance.worlds.search-worlds-placeholder": {
"message": "Search {count} worlds..." "message": "Search {count} worlds..."
}, },
"app.java-detection.cancel": {
"message": "Cancel"
},
"app.java-detection.columns.actions": {
"message": "Actions"
},
"app.java-detection.columns.path": {
"message": "Path"
},
"app.java-detection.columns.version": {
"message": "Version"
},
"app.java-detection.no-installations-found": {
"message": "No Java installations found."
},
"app.java-detection.select": {
"message": "Select"
},
"app.java-detection.selected": {
"message": "Selected"
},
"app.java-detection.title": {
"message": "Select Java installation"
},
"app.java-selector.already-installed": {
"message": "Already installed"
},
"app.java-selector.browse": {
"message": "Browse"
},
"app.java-selector.detect": {
"message": "Detect"
},
"app.java-selector.install-recommended": {
"message": "Install recommended"
},
"app.java-selector.installing": {
"message": "Installing..."
},
"app.java-selector.path.placeholder": {
"message": "/path/to/java"
},
"app.java-selector.test-installation": {
"message": "Test Java installation"
},
"app.modal.install-to-play.additional-context": { "app.modal.install-to-play.additional-context": {
"message": "Additional context" "message": "Additional context"
}, },
@@ -704,11 +755,86 @@
"app.project.versions.already-installed": { "app.project.versions.already-installed": {
"message": "Already installed" "message": "Already installed"
}, },
"app.resource-management-settings.always-show-copy-details.description": { "app.settings.app-version": {
"message": "Show the Copy details action while an install is queued or running. It is always available for failed or interrupted installs." "message": "Modrinth App {version}"
}, },
"app.resource-management-settings.always-show-copy-details.title": { "app.settings.default-instance-options.environment-variables.description": {
"message": "Always show copy details" "message": "Environment variables set when launching an instance."
},
"app.settings.default-instance-options.environment-variables.placeholder": {
"message": "Enter environment variables..."
},
"app.settings.default-instance-options.environment-variables.title": {
"message": "Environment variables"
},
"app.settings.default-instance-options.fullscreen.description": {
"message": "Start instances in fullscreen by updating their options.txt file."
},
"app.settings.default-instance-options.fullscreen.title": {
"message": "Fullscreen"
},
"app.settings.default-instance-options.height.description": {
"message": "The height of the game window when launched."
},
"app.settings.default-instance-options.height.placeholder": {
"message": "Enter height..."
},
"app.settings.default-instance-options.height.title": {
"message": "Height"
},
"app.settings.default-instance-options.java-arguments.description": {
"message": "Arguments passed to Java when launching an instance."
},
"app.settings.default-instance-options.java-arguments.placeholder": {
"message": "Enter Java arguments..."
},
"app.settings.default-instance-options.java-arguments.title": {
"message": "Java arguments"
},
"app.settings.default-instance-options.memory-allocation.description": {
"message": "Maximum memory available to each instance."
},
"app.settings.default-instance-options.memory-allocation.title": {
"message": "Memory allocation"
},
"app.settings.default-instance-options.post-exit-hook.description": {
"message": "Runs after the game closes."
},
"app.settings.default-instance-options.post-exit-hook.placeholder": {
"message": "Enter post-exit command..."
},
"app.settings.default-instance-options.post-exit-hook.title": {
"message": "Post-exit hook"
},
"app.settings.default-instance-options.pre-launch-hook.description": {
"message": "Runs before the instance starts."
},
"app.settings.default-instance-options.pre-launch-hook.placeholder": {
"message": "Enter pre-launch command..."
},
"app.settings.default-instance-options.pre-launch-hook.title": {
"message": "Pre-launch hook"
},
"app.settings.default-instance-options.width.description": {
"message": "The width of the game window when launched."
},
"app.settings.default-instance-options.width.placeholder": {
"message": "Enter width..."
},
"app.settings.default-instance-options.width.title": {
"message": "Width"
},
"app.settings.default-instance-options.wrapper-hook.description": {
"message": "Command used to wrap the Minecraft launch process."
},
"app.settings.default-instance-options.wrapper-hook.placeholder": {
"message": "Enter wrapper command..."
},
"app.settings.default-instance-options.wrapper-hook.title": {
"message": "Wrapper hook"
},
"app.settings.developer-mode-button.label": {
"message": "Toggle developer mode"
}, },
"app.settings.developer-mode-enabled": { "app.settings.developer-mode-enabled": {
"message": "Developer mode enabled." "message": "Developer mode enabled."
@@ -716,12 +842,90 @@
"app.settings.downloading": { "app.settings.downloading": {
"message": "Downloading v{version}" "message": "Downloading v{version}"
}, },
"app.settings.java-installations.location.title": {
"message": "Java {version, number} location"
},
"app.settings.operating-system.macos": {
"message": "macOS"
},
"app.settings.privacy.ads-consent.intro": { "app.settings.privacy.ads-consent.intro": {
"message": "Ads make Modrinth possible and fund creator payouts. Our partners may store or access cookies in the app to personalize ads and measure performance. You can opt out or manage your preferences below." "message": "Ads make Modrinth possible and fund creator payouts. Our partners may store or access cookies in the app to personalize ads and measure performance. You can opt out or manage your preferences below."
}, },
"app.settings.privacy.discord-rich-presence.description": {
"message": "Show Modrinth App as your current activity on Discord. This does not affect Rich Presence added to instances by mods. Requires an app restart."
},
"app.settings.privacy.discord-rich-presence.title": {
"message": "Discord Rich Presence"
},
"app.settings.privacy.telemetry.description": {
"message": "Modrinth collects anonymized analytics and usage data to improve our user experience and customize your experience. By disabling this option, you opt out and your data will no longer be collected."
},
"app.settings.privacy.telemetry.title": {
"message": "Telemetry"
},
"app.settings.resource-management.always-show-copy-details.description": {
"message": "Show the Copy details action while an install is queued or running. It is always available for failed or interrupted installs."
},
"app.settings.resource-management.always-show-copy-details.title": {
"message": "Always show copy details"
},
"app.settings.resource-management.app-cache.confirm.description": {
"message": "The app may load more slowly until the cache is rebuilt."
},
"app.settings.resource-management.app-cache.confirm.title": {
"message": "Purge the app cache?"
},
"app.settings.resource-management.app-cache.description": {
"message": "Clear cached data and download it again from Modrinth. The app may load more slowly until the cache is rebuilt."
},
"app.settings.resource-management.app-cache.purge": {
"message": "Purge cache"
},
"app.settings.resource-management.app-cache.title": {
"message": "App cache"
},
"app.settings.resource-management.app-database-backups.description": {
"message": "Backups of important app data are stored here in case you need to recover them later."
},
"app.settings.resource-management.app-database-backups.open-folder": {
"message": "Open backups folder"
},
"app.settings.resource-management.app-database-backups.title": {
"message": "App database backups"
},
"app.settings.resource-management.app-directory.browse": {
"message": "Browse for an app directory"
},
"app.settings.resource-management.app-directory.description": {
"message": "Where Modrinth App stores instances and other files. Changes take effect after restarting the app."
},
"app.settings.resource-management.app-directory.select": {
"message": "Select a new app directory"
},
"app.settings.resource-management.app-directory.title": {
"message": "App directory"
},
"app.settings.resource-management.maximum-concurrent-downloads.description": {
"message": "Number of files the app can download at once. Lower this if downloads are unreliable on your connection. Requires an app restart."
},
"app.settings.resource-management.maximum-concurrent-downloads.title": {
"message": "Maximum concurrent downloads"
},
"app.settings.resource-management.maximum-concurrent-writes.description": {
"message": "Number of files the app can write to disk at once. Lower this if you frequently encounter I/O errors. Requires an app restart."
},
"app.settings.resource-management.maximum-concurrent-writes.title": {
"message": "Maximum concurrent writes"
},
"app.settings.sidebar.label.instances": {
"message": "Instances"
},
"app.settings.tabs.appearance": { "app.settings.tabs.appearance": {
"message": "Appearance" "message": "Appearance"
}, },
"app.settings.tabs.behavior": {
"message": "Behavior"
},
"app.settings.tabs.default-instance-options": { "app.settings.tabs.default-instance-options": {
"message": "Default instance options" "message": "Default instance options"
}, },
@@ -1564,5 +1768,11 @@
}, },
"search.filter.locked.server-loader.title": { "search.filter.locked.server-loader.title": {
"message": "Loader is provided by the server" "message": "Loader is provided by the server"
},
"settings.sidebar.label.account": {
"message": "Account"
},
"settings.sidebar.label.display": {
"message": "Display"
} }
} }
@@ -173,9 +173,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Decoraciones nativas" "message": "Decoraciones nativas"
}, },
"app.appearance-settings.select-option": {
"message": "Selecciona una opción"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Muestra cuánto tiempo has estado jugando en una instancia." "message": "Muestra cuánto tiempo has estado jugando en una instancia."
}, },
@@ -107,9 +107,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Decoraciones nativas" "message": "Decoraciones nativas"
}, },
"app.appearance-settings.select-option": {
"message": "Selecciona una opción"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Muestra cuánto tiempo has estado jugando en una instancia." "message": "Muestra cuánto tiempo has estado jugando en una instancia."
}, },
@@ -119,9 +119,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Natiivit koristukset" "message": "Natiivit koristukset"
}, },
"app.appearance-settings.select-option": {
"message": "Valitse vaihtoehto"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Näyttää, kuinka paljon aikaa olet käyttänyt instanssin pelaamiseen." "message": "Näyttää, kuinka paljon aikaa olet käyttänyt instanssin pelaamiseen."
}, },
@@ -95,9 +95,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Native decorations" "message": "Native decorations"
}, },
"app.appearance-settings.select-option": {
"message": "Pumili ng opsiyon"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Magpapakita ang katagal ng iyong paglalaro sa isang instansiya." "message": "Magpapakita ang katagal ng iyong paglalaro sa isang instansiya."
}, },
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Décorations natives" "message": "Décorations natives"
}, },
"app.appearance-settings.select-option": {
"message": "Sélectionnez une option"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Affiche combien de temps vous avez passé sur une instance." "message": "Affiche combien de temps vous avez passé sur une instance."
}, },
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Rendszer ablakkeret használata" "message": "Rendszer ablakkeret használata"
}, },
"app.appearance-settings.select-option": {
"message": "Válassz egy lehetőséget"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Megmutatja, mennyi időt töltöttél egy játékpéldányban." "message": "Megmutatja, mennyi időt töltöttél egy játékpéldányban."
}, },
@@ -92,9 +92,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Hiasan asli" "message": "Hiasan asli"
}, },
"app.appearance-settings.select-option": {
"message": "Pilih opsi"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Menampilkan seberapa lama Anda memainkan sebuah instans." "message": "Menampilkan seberapa lama Anda memainkan sebuah instans."
}, },
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Decorazioni native" "message": "Decorazioni native"
}, },
"app.appearance-settings.select-option": {
"message": "Seleziona"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Mostra quanto tempo hai speso giocando a un'istanza." "message": "Mostra quanto tempo hai speso giocando a un'istanza."
}, },
@@ -188,9 +188,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "OSのウィンドウUIを使用" "message": "OSのウィンドウUIを使用"
}, },
"app.appearance-settings.select-option": {
"message": "オプションを選択してください"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "インスタンスのプレイ時間を表示します。" "message": "インスタンスのプレイ時間を表示します。"
}, },
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "네이티브 제목 표시줄" "message": "네이티브 제목 표시줄"
}, },
"app.appearance-settings.select-option": {
"message": "옵션 선택"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "인스턴스의 플레이 시간을 표시합니다." "message": "인스턴스의 플레이 시간을 표시합니다."
}, },
@@ -92,9 +92,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Hiasan asli" "message": "Hiasan asli"
}, },
"app.appearance-settings.select-option": {
"message": "Pilih satu pilihan"
},
"app.appearance-settings.skip-non-essential-warnings.description": { "app.appearance-settings.skip-non-essential-warnings.description": {
"message": "Melangkau pengesahan berisiko rendah secara automatik seperti pemasangan pek mod pendua, pemadaman kandungan biasa, kemas kini pukal, menyahpautkan pek mod dan gesaan pembaikan. Amaran berbahaya masih akan ditunjukkan." "message": "Melangkau pengesahan berisiko rendah secara automatik seperti pemasangan pek mod pendua, pemadaman kandungan biasa, kemas kini pukal, menyahpautkan pek mod dan gesaan pembaikan. Amaran berbahaya masih akan ditunjukkan."
}, },
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Oorspronkelijke versieringen" "message": "Oorspronkelijke versieringen"
}, },
"app.appearance-settings.select-option": {
"message": "Selecteer een optie"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Toont hoeveel tijd je aan een instantie hebt besteed." "message": "Toont hoeveel tijd je aan een instantie hebt besteed."
}, },
@@ -89,9 +89,6 @@
"app.appearance-settings.minimize-launcher.title": { "app.appearance-settings.minimize-launcher.title": {
"message": "Minimer launcheren" "message": "Minimer launcheren"
}, },
"app.appearance-settings.select-option": {
"message": "Velg et alternativ"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Viser hvor lenge du har spilt et tilfelle." "message": "Viser hvor lenge du har spilt et tilfelle."
}, },
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Systemowe dekoracje" "message": "Systemowe dekoracje"
}, },
"app.appearance-settings.select-option": {
"message": "Wybierz opcję"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Pokazuje czas gry spędzony w danej instancji." "message": "Pokazuje czas gry spędzony w danej instancji."
}, },
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Decorações nativas" "message": "Decorações nativas"
}, },
"app.appearance-settings.select-option": {
"message": "Selecione uma opção"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Exibe a quantidade de tempo jogado de uma instância." "message": "Exibe a quantidade de tempo jogado de uma instância."
}, },
@@ -188,9 +188,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Decorações nativas" "message": "Decorações nativas"
}, },
"app.appearance-settings.select-option": {
"message": "Seleciona uma opção"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Apresenta quanto tempo passou a jogar uma instância." "message": "Apresenta quanto tempo passou a jogar uma instância."
}, },
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Системное оформление" "message": "Системное оформление"
}, },
"app.appearance-settings.select-option": {
"message": "Выберите вариант"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Показывать время, проведённое в сборке." "message": "Показывать время, проведённое в сборке."
}, },
@@ -173,9 +173,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Zavičajni ukrasi" "message": "Zavičajni ukrasi"
}, },
"app.appearance-settings.select-option": {
"message": "Izaberi opciju"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Pokazuje koliko vremena si proveo igrajući instancu." "message": "Pokazuje koliko vremena si proveo igrajući instancu."
}, },
@@ -173,9 +173,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Inbyggda fönsterdekorationer" "message": "Inbyggda fönsterdekorationer"
}, },
"app.appearance-settings.select-option": {
"message": "Välj ett alternativ"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Visar hur mycket tid du spelat på en instans." "message": "Visar hur mycket tid du spelat på en instans."
}, },
@@ -107,9 +107,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "ของตกแต่งดั้งเดิม" "message": "ของตกแต่งดั้งเดิม"
}, },
"app.appearance-settings.select-option": {
"message": "เลือกตัวเลือก"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "แสดงเวลาที่คุณใช้เล่นในแต่ละอินสแตนซ์" "message": "แสดงเวลาที่คุณใช้เล่นในแต่ละอินสแตนซ์"
}, },
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Yerel pencere öğeleri" "message": "Yerel pencere öğeleri"
}, },
"app.appearance-settings.select-option": {
"message": "Bir seçenek seçin"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Bir oyun oturumunda ne kadar zaman geçirdiğinizi gösterir." "message": "Bir oyun oturumunda ne kadar zaman geçirdiğinizi gösterir."
}, },
@@ -188,9 +188,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Системне оформлення" "message": "Системне оформлення"
}, },
"app.appearance-settings.select-option": {
"message": "Оберіть опцію"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Показує, скільки часу ви провели в цьому профілі." "message": "Показує, скільки часу ви провели в цьому профілі."
}, },
@@ -182,9 +182,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "Giao diện cửa sổ hệ thống" "message": "Giao diện cửa sổ hệ thống"
}, },
"app.appearance-settings.select-option": {
"message": "Chọn một tuỳ chọn"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "Hiển thị thời gian bạn đã dành để chơi bằng một cấu hình." "message": "Hiển thị thời gian bạn đã dành để chơi bằng một cấu hình."
}, },
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "原生窗口" "message": "原生窗口"
}, },
"app.appearance-settings.select-option": {
"message": "选择一个选项"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "显示你在每个实例中游玩的时间。" "message": "显示你在每个实例中游玩的时间。"
}, },
@@ -191,9 +191,6 @@
"app.appearance-settings.native-decorations.title": { "app.appearance-settings.native-decorations.title": {
"message": "原生視窗" "message": "原生視窗"
}, },
"app.appearance-settings.select-option": {
"message": "選擇選項"
},
"app.appearance-settings.show-play-time.description": { "app.appearance-settings.show-play-time.description": {
"message": "顯示你在該實例的遊玩時數。" "message": "顯示你在該實例的遊玩時數。"
}, },
+1
View File
@@ -35,6 +35,7 @@
"@modrinth/tooling-config": "workspace:*", "@modrinth/tooling-config": "workspace:*",
"@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/container-queries": "^0.1.1",
"@types/node": "^24", "@types/node": "^24",
"@typescript-eslint/typescript-estree": "^8.64.0",
"@vue/compiler-dom": "^3.5.26", "@vue/compiler-dom": "^3.5.26",
"@vue/compiler-sfc": "^3.5.26", "@vue/compiler-sfc": "^3.5.26",
"chalk": "^5.6.2", "chalk": "^5.6.2",
@@ -9,6 +9,7 @@ import { useScrollIndicator } from '../../composables/scroll-indicator'
import NewModal from './NewModal.vue' import NewModal from './NewModal.vue'
export interface Tab { export interface Tab {
name: MessageDescriptor name: MessageDescriptor
category?: MessageDescriptor
icon: Component icon: Component
content?: Component content?: Component
href?: string href?: string
@@ -61,6 +62,11 @@ function hide() {
modal.value?.hide() modal.value?.hide()
} }
function startsCategory(index: number) {
const category = visibleTabs.value[index]?.category
return !!category && category.id !== visibleTabs.value[index - 1]?.category?.id
}
defineExpose({ show, hide, selectedTab, setTab }) defineExpose({ show, hide, selectedTab, setTab })
</script> </script>
<template> <template>
@@ -81,26 +87,32 @@ defineExpose({ show, hide, selectedTab, setTab })
<div <div
class="flex flex-col gap-1 border-solid pr-4 border-0 border-r-[1px] border-divider min-w-[200px]" class="flex flex-col gap-1 border-solid pr-4 border-0 border-r-[1px] border-divider min-w-[200px]"
> >
<component <template v-for="(tab, index) in visibleTabs" :key="index">
:is="tab.href ? 'a' : 'button'" <div
v-for="(tab, index) in visibleTabs" v-if="startsCategory(index) && tab.category"
:key="index" class="px-4 pb-1 pt-2 text-xs font-bold uppercase tracking-wide text-secondary"
:href="tab.href ?? undefined"
:target="tab.href ? '_blank' : undefined"
:rel="tab.href ? 'noopener noreferrer' : undefined"
:class="`flex gap-2 items-center text-left rounded-xl px-4 py-2 border-none text-nowrap font-semibold cursor-pointer active:scale-[0.97] transition-all no-underline ${!tab.href && selectedTab === index ? 'bg-button-bgSelected text-button-textSelected' : 'bg-transparent text-button-text hover:bg-button-bg hover:text-contrast'}`"
@click="!tab.href && setTab(index)"
>
<component :is="tab.icon" class="w-4 h-4 flex-shrink-0" />
<span>{{ formatMessage(tab.name) }}</span>
<span
v-if="tab.badge"
class="rounded-full px-1.5 py-0.5 text-xs font-bold bg-brand-highlight text-brand-green"
> >
{{ formatMessage(tab.badge) }} {{ formatMessage(tab.category) }}
</span> </div>
<RightArrowIcon v-if="tab.href" class="size-4 ml-auto" /> <component
</component> :is="tab.href ? 'a' : 'button'"
:href="tab.href ?? undefined"
:target="tab.href ? '_blank' : undefined"
:rel="tab.href ? 'noopener noreferrer' : undefined"
:class="`flex gap-2 items-center text-left rounded-xl px-4 py-2 border-none text-nowrap font-semibold cursor-pointer active:scale-[0.97] transition-all no-underline ${!tab.href && selectedTab === index ? 'bg-button-bgSelected text-button-textSelected' : 'bg-transparent text-button-text hover:bg-button-bg hover:text-contrast'}`"
@click="!tab.href && setTab(index)"
>
<component :is="tab.icon" class="w-4 h-4 flex-shrink-0" />
<span>{{ formatMessage(tab.name) }}</span>
<span
v-if="tab.badge"
class="rounded-full px-1.5 py-0.5 text-xs font-bold bg-brand-highlight text-brand-green"
>
{{ formatMessage(tab.badge) }}
</span>
<RightArrowIcon v-if="tab.href" class="size-4 ml-auto" />
</component>
</template>
<slot name="footer" /> <slot name="footer" />
</div> </div>
@@ -232,46 +232,55 @@ export const ManyTabs: StoryObj = {
const tabs = [ const tabs = [
{ {
name: { id: 'general', defaultMessage: 'General' }, name: { id: 'general', defaultMessage: 'General' },
category: { id: 'display-category', defaultMessage: 'Display' },
icon: InfoIcon, icon: InfoIcon,
content: makeTabContent('General'), content: makeTabContent('General'),
}, },
{ {
name: { id: 'appearance', defaultMessage: 'Appearance' }, name: { id: 'appearance', defaultMessage: 'Appearance' },
category: { id: 'display-category', defaultMessage: 'Display' },
icon: PaintbrushIcon, icon: PaintbrushIcon,
content: makeTabContent('Appearance'), content: makeTabContent('Appearance'),
}, },
{ {
name: { id: 'language', defaultMessage: 'Language' }, name: { id: 'language', defaultMessage: 'Language' },
category: { id: 'display-category', defaultMessage: 'Display' },
icon: LanguagesIcon, icon: LanguagesIcon,
content: makeTabContent('Language'), content: makeTabContent('Language'),
}, },
{ {
name: { id: 'privacy', defaultMessage: 'Privacy' }, name: { id: 'privacy', defaultMessage: 'Privacy' },
category: { id: 'account-category', defaultMessage: 'Account' },
icon: ShieldIcon, icon: ShieldIcon,
content: makeTabContent('Privacy'), content: makeTabContent('Privacy'),
}, },
{ {
name: { id: 'java', defaultMessage: 'Java and memory' }, name: { id: 'java', defaultMessage: 'Java and memory' },
category: { id: 'instances-category', defaultMessage: 'Instances' },
icon: CoffeeIcon, icon: CoffeeIcon,
content: makeTabContent('Java and memory'), content: makeTabContent('Java and memory'),
}, },
{ {
name: { id: 'instances', defaultMessage: 'Default instance options' }, name: { id: 'instances', defaultMessage: 'Default instance options' },
category: { id: 'instances-category', defaultMessage: 'Instances' },
icon: GameIcon, icon: GameIcon,
content: makeTabContent('Default instance options'), content: makeTabContent('Default instance options'),
}, },
{ {
name: { id: 'resources', defaultMessage: 'Resource management' }, name: { id: 'resources', defaultMessage: 'Resource management' },
category: { id: 'instances-category', defaultMessage: 'Instances' },
icon: GaugeIcon, icon: GaugeIcon,
content: makeTabContent('Resource management'), content: makeTabContent('Resource management'),
}, },
{ {
name: { id: 'window', defaultMessage: 'Window' }, name: { id: 'window', defaultMessage: 'Window' },
category: { id: 'advanced-category', defaultMessage: 'Advanced' },
icon: MonitorIcon, icon: MonitorIcon,
content: makeTabContent('Window'), content: makeTabContent('Window'),
}, },
{ {
name: { id: 'hooks', defaultMessage: 'Launch hooks' }, name: { id: 'hooks', defaultMessage: 'Launch hooks' },
category: { id: 'advanced-category', defaultMessage: 'Advanced' },
icon: WrenchIcon, icon: WrenchIcon,
content: makeTabContent('Launch hooks'), content: makeTabContent('Launch hooks'),
}, },
+84 -20
View File
@@ -31,6 +31,9 @@ importers:
'@types/node': '@types/node':
specifier: ^24 specifier: ^24
version: 24.12.2 version: 24.12.2
'@typescript-eslint/typescript-estree':
specifier: ^8.64.0
version: 8.64.0(typescript@5.9.3)
'@vue/compiler-dom': '@vue/compiler-dom':
specifier: ^3.5.26 specifier: ^3.5.26
version: 3.5.27 version: 3.5.27
@@ -4536,6 +4539,12 @@ packages:
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.0.0' typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/project-service@8.64.0':
resolution: {integrity: sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/scope-manager@8.54.0': '@typescript-eslint/scope-manager@8.54.0':
resolution: {integrity: sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==} resolution: {integrity: sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -4546,6 +4555,12 @@ packages:
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.0.0' typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/tsconfig-utils@8.64.0':
resolution: {integrity: sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/type-utils@8.54.0': '@typescript-eslint/type-utils@8.54.0':
resolution: {integrity: sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==} resolution: {integrity: sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -4557,12 +4572,22 @@ packages:
resolution: {integrity: sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==} resolution: {integrity: sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.64.0':
resolution: {integrity: sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.54.0': '@typescript-eslint/typescript-estree@8.54.0':
resolution: {integrity: sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==} resolution: {integrity: sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.0.0' typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/typescript-estree@8.64.0':
resolution: {integrity: sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/utils@8.54.0': '@typescript-eslint/utils@8.54.0':
resolution: {integrity: sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==} resolution: {integrity: sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -4574,6 +4599,10 @@ packages:
resolution: {integrity: sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==} resolution: {integrity: sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/visitor-keys@8.64.0':
resolution: {integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@ungap/structured-clone@1.3.0': '@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
@@ -6144,6 +6173,10 @@ packages:
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-visitor-keys@5.0.1:
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint@9.39.2: eslint@9.39.2:
resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -9249,6 +9282,12 @@ packages:
peerDependencies: peerDependencies:
typescript: '>=4.8.4' typescript: '>=4.8.4'
ts-api-utils@2.5.0:
resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
engines: {node: '>=18.12'}
peerDependencies:
typescript: '>=4.8.4'
ts-dedent@2.2.0: ts-dedent@2.2.0:
resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
engines: {node: '>=6.10'} engines: {node: '>=6.10'}
@@ -9953,8 +9992,8 @@ packages:
vue-component-type-helpers@3.2.4: vue-component-type-helpers@3.2.4:
resolution: {integrity: sha512-05lR16HeZDcDpB23ku5b5f1fBOoHqFnMiKRr2CiEvbG5Ux4Yi0McmQBOET0dR0nxDXosxyVqv67q6CzS3AK8rw==} resolution: {integrity: sha512-05lR16HeZDcDpB23ku5b5f1fBOoHqFnMiKRr2CiEvbG5Ux4Yi0McmQBOET0dR0nxDXosxyVqv67q6CzS3AK8rw==}
vue-component-type-helpers@3.3.5: vue-component-type-helpers@3.3.7:
resolution: {integrity: sha512-Fe1jyPJoUGpJOYKOri44jduR7My4yYINOMJISuMAbmrs+L5LbIDUc8NTWZYY3EJLK0yPLuCmcd5zoCsE4k2/KA==} resolution: {integrity: sha512-Skkhw9agYSgsWqv7bxSOGJZa9SaiJbZVGdXuFWnrzKaQYHnw9qbjD630rw6RyMqDbp54nfLCLw5SZA55if7JLg==}
vue-confetti-explosion@1.0.2: vue-confetti-explosion@1.0.2:
resolution: {integrity: sha512-80OboM3/6BItIoZ6DpNcZFqGpF607kjIVc5af56oKgtFmt5yWehvJeoYhkzYlqxrqdBe0Ko4Ie3bWrmLau+dJw==} resolution: {integrity: sha512-80OboM3/6BItIoZ6DpNcZFqGpF607kjIVc5af56oKgtFmt5yWehvJeoYhkzYlqxrqdBe0Ko4Ie3bWrmLau+dJw==}
@@ -13436,7 +13475,7 @@ snapshots:
storybook: 10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) storybook: 10.2.4(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
type-fest: 2.19.0 type-fest: 2.19.0
vue: 3.5.27(typescript@5.9.3) vue: 3.5.27(typescript@5.9.3)
vue-component-type-helpers: 3.3.5 vue-component-type-helpers: 3.3.7
'@stripe/stripe-js@7.9.0': {} '@stripe/stripe-js@7.9.0': {}
@@ -13891,6 +13930,15 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/project-service@8.64.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.64.0(typescript@5.9.3)
'@typescript-eslint/types': 8.64.0
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.54.0': '@typescript-eslint/scope-manager@8.54.0':
dependencies: dependencies:
'@typescript-eslint/types': 8.54.0 '@typescript-eslint/types': 8.54.0
@@ -13900,6 +13948,10 @@ snapshots:
dependencies: dependencies:
typescript: 5.9.3 typescript: 5.9.3
'@typescript-eslint/tsconfig-utils@8.64.0(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/type-utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': '@typescript-eslint/type-utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/types': 8.54.0 '@typescript-eslint/types': 8.54.0
@@ -13914,6 +13966,8 @@ snapshots:
'@typescript-eslint/types@8.54.0': {} '@typescript-eslint/types@8.54.0': {}
'@typescript-eslint/types@8.64.0': {}
'@typescript-eslint/typescript-estree@8.54.0(typescript@5.9.3)': '@typescript-eslint/typescript-estree@8.54.0(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/project-service': 8.54.0(typescript@5.9.3) '@typescript-eslint/project-service': 8.54.0(typescript@5.9.3)
@@ -13929,6 +13983,21 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/typescript-estree@8.64.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.64.0(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.64.0(typescript@5.9.3)
'@typescript-eslint/types': 8.64.0
'@typescript-eslint/visitor-keys': 8.64.0
debug: 4.4.3
minimatch: 10.2.3
semver: 7.7.4
tinyglobby: 0.2.15
ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': '@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7)) '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
@@ -13956,6 +14025,11 @@ snapshots:
'@typescript-eslint/types': 8.54.0 '@typescript-eslint/types': 8.54.0
eslint-visitor-keys: 4.2.1 eslint-visitor-keys: 4.2.1
'@typescript-eslint/visitor-keys@8.64.0':
dependencies:
'@typescript-eslint/types': 8.64.0
eslint-visitor-keys: 5.0.1
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
'@unhead/vue@2.1.2(vue@3.5.27(typescript@5.9.3))': '@unhead/vue@2.1.2(vue@3.5.27(typescript@5.9.3))':
@@ -15815,6 +15889,8 @@ snapshots:
eslint-visitor-keys@4.2.1: {} eslint-visitor-keys@4.2.1: {}
eslint-visitor-keys@5.0.1: {}
eslint@9.39.2(jiti@1.21.7): eslint@9.39.2(jiti@1.21.7):
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7)) '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
@@ -19773,6 +19849,10 @@ snapshots:
dependencies: dependencies:
typescript: 5.9.3 typescript: 5.9.3
ts-api-utils@2.5.0(typescript@5.9.3):
dependencies:
typescript: 5.9.3
ts-dedent@2.2.0: {} ts-dedent@2.2.0: {}
ts-interface-checker@0.1.13: {} ts-interface-checker@0.1.13: {}
@@ -20277,22 +20357,6 @@ snapshots:
yaml: 2.8.2 yaml: 2.8.2
optional: true optional: true
vite@8.0.3(@types/node@24.12.2)(esbuild@0.27.3)(jiti@1.21.7)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
postcss: 8.5.8
rolldown: 1.0.0-rc.12
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.12.2
esbuild: 0.27.3
fsevents: 2.3.3
jiti: 1.21.7
sass: 1.97.3
terser: 5.46.0
yaml: 2.8.2
vite@8.0.3(@types/node@24.12.2)(esbuild@0.27.3)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2): vite@8.0.3(@types/node@24.12.2)(esbuild@0.27.3)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2):
dependencies: dependencies:
lightningcss: 1.32.0 lightningcss: 1.32.0
@@ -20429,7 +20493,7 @@ snapshots:
vue-component-type-helpers@3.2.4: {} vue-component-type-helpers@3.2.4: {}
vue-component-type-helpers@3.3.5: {} vue-component-type-helpers@3.3.7: {}
vue-confetti-explosion@1.0.2(vue@3.5.27(typescript@5.9.3)): vue-confetti-explosion@1.0.2(vue@3.5.27(typescript@5.9.3)):
dependencies: dependencies: