From 3e07267e10ee687c25ccc5dfe3a77fdd00088000 Mon Sep 17 00:00:00 2001
From: "Calum H."
Date: Thu, 20 Aug 2026 08:23:01 +0100
Subject: [PATCH] feat: preferences syncing frontend (#7192)
* feat: prefs frontend
* fix: DI issue
* fix: lint
* feat: appearance settings cleanup + fix settings, remove pinia
* fix: sync btn when logged out
* fix: prepr
* fix: sidebar issue
* feat: language coverage + cleanup
* feat: cleanup lang settings
* fix: fmt
* fix: CI
* fix: ci
* fix: storybook
* feat: bring back loader/game version sort
---------
Co-authored-by: tdgao
---
.github/workflows/frontend-deploy.yml | 3 +-
.github/workflows/frontend-preview.yml | 3 +-
.gitignore | 1 +
apps/app-frontend/package.json | 1 -
apps/app-frontend/src/App.vue | 118 +++-
.../src/components/ui/AccountsCard.vue | 2 +-
.../src/components/ui/ErrorModal.vue | 2 +-
.../src/components/ui/Instance.vue | 2 +-
.../src/components/ui/WindowControls.vue | 6 +-
.../src/components/ui/friends/FriendsList.vue | 8 +-
.../install_flow/UnknownPackWarningModal.vue | 9 +-
.../library/instance-group/instance-card.vue | 2 +-
.../ui/library/library-toolbar/sort-menu.vue | 5 +
.../src/components/ui/library/use-library.ts | 14 +
.../MinecraftAuthErrorModal.vue | 2 +-
.../MinecraftRequiredModal.vue | 2 +-
.../components/ui/modal/AppSettingsModal.vue | 31 +-
.../ui/settings/account/SocialSettings.vue | 20 +
.../settings/display/AppearanceSettings.vue | 228 ++++---
.../ui/settings/display/BehaviorSettings.vue | 239 ++++---
.../settings/display/FeatureFlagSettings.vue | 21 +-
.../ui/settings/display/LanguageSettings.vue | 91 +--
.../instances/ResourceManagementSettings.vue | 12 +-
.../use-shared-instance-invite-handler.ts | 6 +-
.../src/components/ui/world/InstanceItem.vue | 2 +-
.../components/ui/world/RecentWorldsList.vue | 8 +-
.../browse/install-job-notifications.ts | 6 +-
.../src/composables/use-app-settings.ts | 52 ++
.../app-frontend/src/composables/use-error.js | 48 ++
.../app-frontend/src/composables/use-theme.ts | 47 ++
apps/app-frontend/src/helpers/settings.ts | 5 +-
apps/app-frontend/src/helpers/types.d.ts | 2 +
.../src/helpers/user-preferences.ts | 19 +
.../app-frontend/src/locales/ar-SA/index.json | 12 -
.../app-frontend/src/locales/cs-CZ/index.json | 12 -
.../app-frontend/src/locales/da-DK/index.json | 6 -
.../app-frontend/src/locales/de-CH/index.json | 18 -
.../app-frontend/src/locales/de-DE/index.json | 18 -
.../app-frontend/src/locales/en-US/index.json | 33 +-
.../src/locales/es-419/index.json | 18 -
.../app-frontend/src/locales/es-ES/index.json | 12 -
.../app-frontend/src/locales/fi-FI/index.json | 12 -
.../src/locales/fil-PH/index.json | 12 -
.../app-frontend/src/locales/fr-FR/index.json | 18 -
.../app-frontend/src/locales/hu-HU/index.json | 18 -
.../app-frontend/src/locales/id-ID/index.json | 12 -
.../app-frontend/src/locales/it-IT/index.json | 18 -
.../app-frontend/src/locales/ja-JP/index.json | 18 -
.../app-frontend/src/locales/ko-KR/index.json | 18 -
.../app-frontend/src/locales/ms-MY/index.json | 12 -
.../app-frontend/src/locales/nl-NL/index.json | 18 -
.../app-frontend/src/locales/no-NO/index.json | 6 -
.../app-frontend/src/locales/pl-PL/index.json | 18 -
.../app-frontend/src/locales/pt-BR/index.json | 18 -
.../app-frontend/src/locales/pt-PT/index.json | 12 -
.../app-frontend/src/locales/ru-RU/index.json | 18 -
.../app-frontend/src/locales/sr-CS/index.json | 18 -
.../app-frontend/src/locales/sv-SE/index.json | 15 -
.../app-frontend/src/locales/th-TH/index.json | 12 -
.../app-frontend/src/locales/tr-TR/index.json | 18 -
.../app-frontend/src/locales/uk-UA/index.json | 18 -
.../app-frontend/src/locales/vi-VN/index.json | 12 -
.../app-frontend/src/locales/zh-CN/index.json | 18 -
.../app-frontend/src/locales/zh-TW/index.json | 18 -
apps/app-frontend/src/main.js | 4 -
apps/app-frontend/src/pages/Browse.vue | 18 +-
apps/app-frontend/src/pages/Index.vue | 6 +-
apps/app-frontend/src/pages/Skins.vue | 8 +-
.../src/pages/hosting/manage/Index.vue | 6 +-
.../settings-modal/installation-settings.vue | 6 +-
.../src/pages/instance/content/index.vue | 11 +-
.../src/pages/instance/layout.vue | 8 +-
.../src/pages/instance/worlds/index.vue | 2 +-
apps/app-frontend/src/pages/project/Index.vue | 6 +-
.../src/pages/project/Versions.vue | 6 +-
apps/app-frontend/src/plugins/i18n-debug.ts | 8 +-
.../src/providers/content-install.ts | 6 +-
.../src/providers/server-install.ts | 2 +-
apps/app-frontend/src/providers/setup/auth.ts | 1 +
.../src/providers/setup/creation-modal.ts | 6 +-
.../src/providers/setup/loading-state.ts | 2 +-
.../src/providers/setup/user-preferences.ts | 19 +
apps/app-frontend/src/store/error.js | 49 --
apps/app-frontend/src/store/state.js | 3 -
apps/app-frontend/src/store/theme.ts | 91 ---
apps/app/build.rs | 4 +
apps/app/src/api/users.rs | 15 +
apps/app/tauri.conf.json | 2 +-
apps/frontend/src/app.vue | 46 +-
apps/frontend/src/components/ui/NavStack.vue | 36 +-
apps/frontend/src/layouts/default.vue | 16 +-
apps/frontend/src/locales/cs-CZ/index.json | 72 ---
apps/frontend/src/locales/da-DK/index.json | 42 --
apps/frontend/src/locales/de-CH/index.json | 84 ---
apps/frontend/src/locales/de-DE/index.json | 84 ---
apps/frontend/src/locales/en-US/index.json | 84 ---
apps/frontend/src/locales/es-419/index.json | 84 ---
apps/frontend/src/locales/es-ES/index.json | 84 ---
apps/frontend/src/locales/fi-FI/index.json | 15 -
apps/frontend/src/locales/fil-PH/index.json | 84 ---
apps/frontend/src/locales/fr-FR/index.json | 84 ---
apps/frontend/src/locales/hu-HU/index.json | 84 ---
apps/frontend/src/locales/id-ID/index.json | 75 ---
apps/frontend/src/locales/it-IT/index.json | 84 ---
apps/frontend/src/locales/ja-JP/index.json | 84 ---
apps/frontend/src/locales/ko-KR/index.json | 84 ---
apps/frontend/src/locales/ms-MY/index.json | 84 ---
apps/frontend/src/locales/nl-NL/index.json | 84 ---
apps/frontend/src/locales/no-NO/index.json | 84 ---
apps/frontend/src/locales/pl-PL/index.json | 84 ---
apps/frontend/src/locales/pt-BR/index.json | 84 ---
apps/frontend/src/locales/pt-PT/index.json | 84 ---
apps/frontend/src/locales/ro-RO/index.json | 72 ---
apps/frontend/src/locales/ru-RU/index.json | 84 ---
apps/frontend/src/locales/sr-CS/index.json | 84 ---
apps/frontend/src/locales/sv-SE/index.json | 84 ---
apps/frontend/src/locales/tr-TR/index.json | 84 ---
apps/frontend/src/locales/uk-UA/index.json | 84 ---
apps/frontend/src/locales/vi-VN/index.json | 84 ---
apps/frontend/src/locales/zh-CN/index.json | 84 ---
apps/frontend/src/locales/zh-TW/index.json | 84 ---
.../src/pages/discover/[type]/index.vue | 26 +-
apps/frontend/src/pages/settings/index.vue | 603 +++++++-----------
apps/frontend/src/pages/settings/language.vue | 93 +--
apps/frontend/src/pages/settings/social.vue | 15 +
apps/frontend/src/plugins/cosmetics.ts | 1 +
apps/frontend/src/plugins/i18n.ts | 9 +-
apps/frontend/src/plugins/theme/index.ts | 11 +-
.../src/plugins/theme/theme-settings.ts | 7 +
apps/frontend/src/providers/setup.ts | 18 +-
apps/frontend/src/providers/setup/auth.ts | 1 +
apps/labrinth/src/models/v3/preferences.rs | 27 +
package.json | 9 +-
.../api-client/src/modules/labrinth/types.ts | 63 ++
.../src/modules/labrinth/users/v3.ts | 37 ++
...6f5b6a3155788880369af940fcd268d3cb94.json} | 18 +-
...c723b356263ffc83aabb645c9fb5b6d5aa7c.json} | 6 +-
...260818120000_sync-preferences-settings.sql | 5 +
packages/app-lib/src/api/users.rs | 45 ++
packages/app-lib/src/state/settings.rs | 12 +-
packages/ui/src/components/index.ts | 1 -
.../ui/src/components/modal/TabbedModal.vue | 32 +-
.../components/settings/LanguageSelector.vue | 250 --------
packages/ui/src/components/settings/index.ts | 2 -
packages/ui/src/layouts/index.ts | 1 +
.../components/AppearanceSettingRow.vue | 37 ++
.../components/ProjectLayoutSelector.vue | 76 +++
...earance-settings-theme-selector.stories.ts | 31 +
.../appearance-settings-theme-selector.vue} | 59 +-
.../shared/appearance-settings/index.ts | 3 +
.../shared/appearance-settings/layout.vue | 181 ++++++
.../shared/appearance-settings/messages.ts | 108 ++++
.../providers/appearance-settings.ts | 229 +++++++
.../appearance-settings/providers/index.ts | 1 +
.../shared/appearance-settings/types.ts | 36 ++
.../src/layouts/shared/content-tab/layout.vue | 5 +-
.../content-tab/providers/content-manager.ts | 2 +-
packages/ui/src/layouts/wrapped/index.ts | 3 +-
.../{ => settings}/AccountProfileSettings.vue | 0
.../{ => settings}/AccountSocialSettings.vue | 188 +++++-
.../ui/src/layouts/wrapped/settings/index.ts | 3 +
.../settings/language-settings/index.vue | 121 ++++
.../language-settings-coverage.generated.d.ts | 3 +
.../language-settings-coverage.ts | 15 +
.../language-settings-selector.vue | 335 ++++++++++
packages/ui/src/locales/de-CH/index.json | 3 -
packages/ui/src/locales/de-DE/index.json | 3 -
packages/ui/src/locales/en-US/index.json | 112 +++-
packages/ui/src/locales/es-419/index.json | 3 -
packages/ui/src/locales/es-ES/index.json | 3 -
packages/ui/src/locales/fr-FR/index.json | 3 -
packages/ui/src/locales/hu-HU/index.json | 3 -
packages/ui/src/locales/it-IT/index.json | 3 -
packages/ui/src/locales/ja-JP/index.json | 3 -
packages/ui/src/locales/ko-KR/index.json | 3 -
packages/ui/src/locales/nl-NL/index.json | 3 -
packages/ui/src/locales/pl-PL/index.json | 3 -
packages/ui/src/locales/pt-BR/index.json | 3 -
packages/ui/src/locales/ru-RU/index.json | 3 -
packages/ui/src/locales/sv-SE/index.json | 3 -
packages/ui/src/locales/uk-UA/index.json | 3 -
packages/ui/src/locales/zh-CN/index.json | 3 -
packages/ui/src/locales/zh-TW/index.json | 3 -
packages/ui/src/providers/i18n.ts | 2 +-
packages/ui/src/providers/index.ts | 1 +
packages/ui/src/providers/user-preferences.ts | 116 ++++
packages/ui/src/utils/savable.ts | 12 +-
pnpm-lock.yaml | 193 ++----
scripts/coverage-i18n.ts | 215 ++++++-
scripts/run.mjs | 11 +-
turbo.jsonc | 21 +-
191 files changed, 3356 insertions(+), 4372 deletions(-)
create mode 100644 apps/app-frontend/src/composables/use-app-settings.ts
create mode 100644 apps/app-frontend/src/composables/use-error.js
create mode 100644 apps/app-frontend/src/composables/use-theme.ts
create mode 100644 apps/app-frontend/src/helpers/user-preferences.ts
create mode 100644 apps/app-frontend/src/providers/setup/user-preferences.ts
delete mode 100644 apps/app-frontend/src/store/error.js
delete mode 100644 apps/app-frontend/src/store/state.js
delete mode 100644 apps/app-frontend/src/store/theme.ts
rename packages/app-lib/.sqlx/{query-6f8b76b839db48539a70664af549c12be71df20b8cf520019413f6ea751cd594.json => query-29df6a29d9221b9bbea45e024fb26f5b6a3155788880369af940fcd268d3cb94.json} (90%)
rename packages/app-lib/.sqlx/{query-dfdc99cb5ec60bb08c566920628deec7e741eca9490811ebad6594abe8a6126d.json => query-88c6a22aa112f10dce9d42779f06c723b356263ffc83aabb645c9fb5b6d5aa7c.json} (85%)
create mode 100644 packages/app-lib/migrations/20260818120000_sync-preferences-settings.sql
delete mode 100644 packages/ui/src/components/settings/LanguageSelector.vue
delete mode 100644 packages/ui/src/components/settings/index.ts
create mode 100644 packages/ui/src/layouts/shared/appearance-settings/components/AppearanceSettingRow.vue
create mode 100644 packages/ui/src/layouts/shared/appearance-settings/components/ProjectLayoutSelector.vue
create mode 100644 packages/ui/src/layouts/shared/appearance-settings/components/appearance-settings-theme-selector.stories.ts
rename packages/ui/src/{components/settings/ThemeSelector.vue => layouts/shared/appearance-settings/components/appearance-settings-theme-selector.vue} (67%)
create mode 100644 packages/ui/src/layouts/shared/appearance-settings/index.ts
create mode 100644 packages/ui/src/layouts/shared/appearance-settings/layout.vue
create mode 100644 packages/ui/src/layouts/shared/appearance-settings/messages.ts
create mode 100644 packages/ui/src/layouts/shared/appearance-settings/providers/appearance-settings.ts
create mode 100644 packages/ui/src/layouts/shared/appearance-settings/providers/index.ts
create mode 100644 packages/ui/src/layouts/shared/appearance-settings/types.ts
rename packages/ui/src/layouts/wrapped/{ => settings}/AccountProfileSettings.vue (100%)
rename packages/ui/src/layouts/wrapped/{ => settings}/AccountSocialSettings.vue (69%)
create mode 100644 packages/ui/src/layouts/wrapped/settings/index.ts
create mode 100644 packages/ui/src/layouts/wrapped/settings/language-settings/index.vue
create mode 100644 packages/ui/src/layouts/wrapped/settings/language-settings/language-settings-coverage.generated.d.ts
create mode 100644 packages/ui/src/layouts/wrapped/settings/language-settings/language-settings-coverage.ts
create mode 100644 packages/ui/src/layouts/wrapped/settings/language-settings/language-settings-selector.vue
create mode 100644 packages/ui/src/providers/user-preferences.ts
diff --git a/.github/workflows/frontend-deploy.yml b/.github/workflows/frontend-deploy.yml
index 703431d240..79b73c57ef 100644
--- a/.github/workflows/frontend-deploy.yml
+++ b/.github/workflows/frontend-deploy.yml
@@ -125,8 +125,7 @@ jobs:
run: pnpm install
- name: Build frontend
- working-directory: ./apps/frontend
- run: pnpm build
+ run: pnpm web:build
env:
CF_PAGES_BRANCH: ${{ inputs.head_ref || github.ref_name }}
CF_PAGES_COMMIT_SHA: ${{ inputs.head_sha || github.sha }}
diff --git a/.github/workflows/frontend-preview.yml b/.github/workflows/frontend-preview.yml
index df17e1786c..16e76136fd 100644
--- a/.github/workflows/frontend-preview.yml
+++ b/.github/workflows/frontend-preview.yml
@@ -70,10 +70,9 @@ jobs:
run: pnpm install
- name: Build Storybook
- working-directory: ./packages/ui
env:
NODE_OPTIONS: --max-old-space-size=6144
- run: pnpm run build-storybook
+ run: pnpm build-storybook
- name: Configure short SHA
id: meta
diff --git a/.gitignore b/.gitignore
index 9d27dcaf5e..d442251099 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,6 +55,7 @@ Thumbs.db
# frontend generated files
apps/frontend/src/generated
+/packages/ui/src/layouts/wrapped/settings/language-settings/language-settings-coverage.generated.ts
.turbo
target
diff --git a/apps/app-frontend/package.json b/apps/app-frontend/package.json
index 5a88bd2d92..abe6e55c0f 100644
--- a/apps/app-frontend/package.json
+++ b/apps/app-frontend/package.json
@@ -38,7 +38,6 @@
"intl-messageformat": "^10.7.7",
"ofetch": "^1.3.4",
"overlayscrollbars": "^2.15.1",
- "pinia": "^3.0.0",
"posthog-js": "^1.158.2",
"three": "^0.172.0",
"vite-svg-loader": "^5.1.0",
diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue
index eeb37273f6..7456fa2dec 100644
--- a/apps/app-frontend/src/App.vue
+++ b/apps/app-frontend/src/App.vue
@@ -94,6 +94,9 @@ import SurveyPopup from '@/components/ui/SurveyPopup.vue'
import WindowControls from '@/components/ui/WindowControls.vue'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
import { useAppEvent } from '@/composables/use-app-event'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
+import { useError } from '@/composables/use-error.js'
+import { useTheme } from '@/composables/use-theme.ts'
import { config } from '@/config'
import {
hide_ads_window,
@@ -147,8 +150,7 @@ import { setupProviders } from '@/providers/setup'
import { setupAppEventsProvider } from '@/providers/setup/app-events'
import { setupAuthProvider } from '@/providers/setup/auth'
import { setupLoadingStateProvider } from '@/providers/setup/loading-state'
-import { useError } from '@/store/error.js'
-import { useTheming } from '@/store/state'
+import { setupAppUserPreferencesProvider } from '@/providers/setup/user-preferences.ts'
import { appMessages } from '@/utils/app-messages'
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
@@ -157,7 +159,8 @@ import { AppNotificationManager } from './providers/app-notifications'
import { AppPopupNotificationManager } from './providers/app-popup-notifications'
import { appSettingsModalOpenProfileKey } from './providers/app-settings-modal'
-const themeStore = useTheming()
+const appSettings = useAppSettings()
+const appTheme = useTheme()
const router = useRouter()
const route = useRoute()
const { channel: appEventChannel, events: appEvents } = setupAppEventsProvider()
@@ -201,7 +204,7 @@ const credentials = ref()
let credentialsRefreshId = 0
const sidebarToggled = ref(true)
watch(
- () => themeStore.toggleSidebar,
+ () => appSettings.toggleSidebar,
(toggleSidebar) => {
sidebarToggled.value = !toggleSidebar
},
@@ -221,7 +224,7 @@ const hostingUpdateRequired = computed(
appUpdateState.updatesEnabled.value,
)
const prideFundraiserEnabled = computed(
- () => themeStore.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE,
+ () => appSettings.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE,
)
const hostingIntercomIdentityKey = computed(() => {
const rawServerId = route.params.id
@@ -324,13 +327,13 @@ providePageContext({
intercomBubble: hostingIntercom.intercomBubble,
featureFlags: {
serverRamAsBytesAlwaysOn: computed(() =>
- themeStore.getFeatureFlag('server_ram_as_bytes_always_on'),
+ appSettings.getFeatureFlag('server_ram_as_bytes_always_on'),
),
},
openExternalUrl: (url) => void openUrl(url),
})
provideModalBehavior({
- noblur: computed(() => !themeStore.advancedRendering),
+ noblur: computed(() => !appTheme.advancedRendering),
onShow: () => take_ads_window_hold(),
onHide: () => release_ads_window_hold(),
})
@@ -634,10 +637,11 @@ async function setupApp() {
theme,
locale,
telemetry,
- collapsed_navigation,
hide_nametag_skins_page,
advanced_rendering,
toggle_sidebar,
+ sync_theme_across_devices,
+ sync_behavior_across_devices,
developer_mode,
feature_flags,
pending_update_toast_for_version,
@@ -655,13 +659,14 @@ async function setupApp() {
nativeDecorations.value = native_decorations
if (os.value !== 'MacOS') await getCurrentWindow().setDecorations(native_decorations)
- themeStore.setThemeState(theme)
- themeStore.collapsedNavigation = collapsed_navigation
- themeStore.advancedRendering = advanced_rendering
- themeStore.hideNametagSkinsPage = hide_nametag_skins_page
- themeStore.toggleSidebar = toggle_sidebar
- themeStore.devMode = developer_mode
- themeStore.featureFlags = feature_flags
+ appTheme.preferred = theme
+ appTheme.advancedRendering = advanced_rendering
+ appTheme.syncAcrossDevices = sync_theme_across_devices
+ appSettings.syncBehaviorAcrossDevices = sync_behavior_across_devices
+ appSettings.hideNametagSkinsPage = hide_nametag_skins_page
+ appSettings.toggleSidebar = toggle_sidebar
+ appSettings.devMode = developer_mode
+ Object.assign(appSettings.featureFlags, feature_flags)
stateInitialized.value = true
isMaximized.value = await getCurrentWindow().isMaximized()
@@ -959,7 +964,7 @@ watch(incompatibilityWarningModal, (modal) => {
}
})
-setupAuthProvider(credentials, async (_redirectPath, flow, options) => {
+const authProvider = setupAuthProvider(credentials, async (_redirectPath, flow, options) => {
if (options?.showModal === false) {
await signIn(flow)
} else {
@@ -967,6 +972,79 @@ setupAuthProvider(credentials, async (_redirectPath, flow, options) => {
}
})
+const userPreferences = setupAppUserPreferencesProvider(authProvider, notificationManager)
+let userPreferencesSync = Promise.resolve()
+
+watch(
+ [userPreferences.preferences, stateInitialized],
+ ([preferences, initialized]) => {
+ if (!preferences || !initialized) return
+
+ userPreferencesSync = userPreferencesSync
+ .then(async () => {
+ const settings = await getSettings()
+ const selectedTheme = preferences.appearance.auto ? 'system' : preferences.appearance.theme
+ const locale = preferences.localization.locale
+ const behavior = preferences.behavior
+ let settingsChanged = false
+
+ if (appTheme.syncAcrossDevices && appTheme.preferred !== selectedTheme) {
+ appTheme.preferred = selectedTheme
+ }
+ if (i18n.global.locale.value !== locale) {
+ i18n.global.locale.value = locale
+ }
+
+ if (appTheme.syncAcrossDevices && settings.theme !== selectedTheme) {
+ settings.theme = selectedTheme
+ settingsChanged = true
+ }
+ if (settings.locale !== locale) {
+ settings.locale = locale
+ settingsChanged = true
+ }
+
+ if (behavior && appSettings.syncBehaviorAcrossDevices) {
+ const behaviorFeatureFlags = {
+ worlds_in_home: behavior.show_jump_in,
+ show_instance_play_time: behavior.show_play_time,
+ skip_unknown_pack_warning: !behavior.warn_on_unknown_modpacks,
+ skip_non_essential_warnings: behavior.skip_non_essential_warnings,
+ }
+
+ appSettings.toggleSidebar = behavior.hide_right_sidebar
+ appSettings.hideNametagSkinsPage = behavior.hide_nametag
+ Object.assign(appSettings.featureFlags, behaviorFeatureFlags)
+
+ if (settings.hide_on_process_start !== behavior.minimize_app) {
+ settings.hide_on_process_start = behavior.minimize_app
+ settingsChanged = true
+ }
+ if (settings.toggle_sidebar !== behavior.hide_right_sidebar) {
+ settings.toggle_sidebar = behavior.hide_right_sidebar
+ settingsChanged = true
+ }
+ if (settings.hide_nametag_skins_page !== behavior.hide_nametag) {
+ settings.hide_nametag_skins_page = behavior.hide_nametag
+ settingsChanged = true
+ }
+ for (const [flag, value] of Object.entries(behaviorFeatureFlags)) {
+ if (settings.feature_flags[flag] !== value) {
+ settings.feature_flags[flag] = value
+ settingsChanged = true
+ }
+ }
+ }
+
+ if (settingsChanged) {
+ await setSettings(settings)
+ }
+ })
+ .catch(handleError)
+ },
+ { immediate: true },
+)
+
async function validateSession(sessionToken) {
try {
const response = await tauriFetch(`${config.labrinthBaseUrl}/v2/user`, {
@@ -1642,7 +1720,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
@@ -1867,7 +1945,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
{{ route.fullPath }}
diff --git a/apps/app-frontend/src/components/ui/AccountsCard.vue b/apps/app-frontend/src/components/ui/AccountsCard.vue
index ac9136f6c2..9e3631192d 100644
--- a/apps/app-frontend/src/components/ui/AccountsCard.vue
+++ b/apps/app-frontend/src/components/ui/AccountsCard.vue
@@ -107,6 +107,7 @@ import type { Ref } from 'vue'
import { computed, ref } from 'vue'
import { useAppEvent } from '@/composables/use-app-event'
+import { handleSevereError } from '@/composables/use-error.js'
import { trackEvent } from '@/helpers/analytics'
import {
get_default_user,
@@ -118,7 +119,6 @@ import {
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
import type { Skin } from '@/helpers/skins'
import { get_available_skins } from '@/helpers/skins'
-import { handleSevereError } from '@/store/error.js'
const { formatMessage } = useVIntl()
const { handleError } = injectNotificationManager()
diff --git a/apps/app-frontend/src/components/ui/ErrorModal.vue b/apps/app-frontend/src/components/ui/ErrorModal.vue
index 475c1f3f76..efac61720c 100644
--- a/apps/app-frontend/src/components/ui/ErrorModal.vue
+++ b/apps/app-frontend/src/components/ui/ErrorModal.vue
@@ -20,11 +20,11 @@ import { computed, ref } from 'vue'
import { ChatIcon } from '@/assets/icons'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
+import { handleSevereError } from '@/composables/use-error.js'
import { trackEvent } from '@/helpers/analytics'
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
import { install_existing_instance } from '@/helpers/install'
import { cancel_directory_change } from '@/helpers/settings.ts'
-import { handleSevereError } from '@/store/error.js'
const { handleError } = injectNotificationManager()
diff --git a/apps/app-frontend/src/components/ui/Instance.vue b/apps/app-frontend/src/components/ui/Instance.vue
index 2bc32b67fc..9f8338df6d 100644
--- a/apps/app-frontend/src/components/ui/Instance.vue
+++ b/apps/app-frontend/src/components/ui/Instance.vue
@@ -20,12 +20,12 @@ import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAppEvent } from '@/composables/use-app-event'
+import { handleSevereError } from '@/composables/use-error.js'
import { trackEvent } from '@/helpers/analytics'
import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install'
import { getInstanceIconUrl, kill, run } from '@/helpers/instance'
import { get_by_instance_id } from '@/helpers/process'
import { showInstanceInFolder } from '@/helpers/utils.js'
-import { handleSevereError } from '@/store/error.js'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
diff --git a/apps/app-frontend/src/components/ui/WindowControls.vue b/apps/app-frontend/src/components/ui/WindowControls.vue
index 81797ba5fa..a10ccffff5 100644
--- a/apps/app-frontend/src/components/ui/WindowControls.vue
+++ b/apps/app-frontend/src/components/ui/WindowControls.vue
@@ -39,17 +39,17 @@ import { getCurrentWindow } from '@tauri-apps/api/window'
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
import { computed, onMounted, onUnmounted, ref } from 'vue'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
import { get as getSettings } from '@/helpers/settings.ts'
import { getOS } from '@/helpers/utils.js'
-import { useTheming } from '@/store/state'
-const themeStore = useTheming()
+const appSettings = useAppSettings()
const nativeDecorations = ref(true)
const isMaximized = ref(false)
const os = ref('')
-const alwaysShowAppControls = computed(() => themeStore.getFeatureFlag('always_show_app_controls'))
+const alwaysShowAppControls = computed(() => appSettings.getFeatureFlag('always_show_app_controls'))
const showControls = computed(
() =>
diff --git a/apps/app-frontend/src/components/ui/friends/FriendsList.vue b/apps/app-frontend/src/components/ui/friends/FriendsList.vue
index 705b3c16a1..7294781e87 100644
--- a/apps/app-frontend/src/components/ui/friends/FriendsList.vue
+++ b/apps/app-frontend/src/components/ui/friends/FriendsList.vue
@@ -15,17 +15,17 @@ import { computed, ref } from 'vue'
import FriendsSection from '@/components/ui/friends/FriendsSection.vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
import { useFriends } from '@/composables/use-friends'
import type { FriendWithUserData } from '@/helpers/friends.ts'
import type { ModrinthCredentials } from '@/helpers/mr_auth'
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
-import { useTheming } from '@/store/state'
const { formatMessage } = useVIntl()
const { handleError } = injectNotificationManager()
const formatRelativeTime = useRelativeTime()
-const themeStore = useTheming()
+const appSettings = useAppSettings()
const props = defineProps<{
credentials: ModrinthCredentials | null
@@ -39,11 +39,11 @@ type FriendsSectionCollapsedFlag =
| 'friends_pending_collapsed'
function isFriendsSectionCollapsed(flag: FriendsSectionCollapsedFlag) {
- return themeStore.getFeatureFlag(flag)
+ return appSettings.getFeatureFlag(flag)
}
function setFriendsSectionCollapsed(flag: FriendsSectionCollapsedFlag, collapsed: boolean) {
- themeStore.featureFlags[flag] = collapsed
+ appSettings.featureFlags[flag] = collapsed
getSettings()
.then((settings) => {
settings.feature_flags[flag] = collapsed
diff --git a/apps/app-frontend/src/components/ui/install_flow/UnknownPackWarningModal.vue b/apps/app-frontend/src/components/ui/install_flow/UnknownPackWarningModal.vue
index f1b929e050..b1063c805d 100644
--- a/apps/app-frontend/src/components/ui/install_flow/UnknownPackWarningModal.vue
+++ b/apps/app-frontend/src/components/ui/install_flow/UnknownPackWarningModal.vue
@@ -13,11 +13,10 @@
import { UnknownFileWarningModal } from '@modrinth/ui'
import { ref, useTemplateRef } from 'vue'
+import { type FeatureFlag, useAppSettings } from '@/composables/use-app-settings.ts'
import { get as getSettings, set as setSettings } from '@/helpers/settings'
-import { useTheming } from '@/store/state'
-import type { FeatureFlag } from '@/store/theme.ts'
-const themeStore = useTheming()
+const appSettings = useAppSettings()
const skipUnknownPackWarningFeatureFlag = 'skip_unknown_pack_warning' as FeatureFlag
const modal = useTemplateRef('modal')
@@ -34,7 +33,7 @@ function show(
fileName.value = selectedFileName
externalFilesInModpack.value = selectedExternalFiles
- if (themeStore.getFeatureFlag(skipUnknownPackWarningFeatureFlag)) {
+ if (appSettings.getFeatureFlag(skipUnknownPackWarningFeatureFlag)) {
void createInstance()
return
}
@@ -50,7 +49,7 @@ function reset() {
async function proceed(dontShowAgain: boolean) {
if (dontShowAgain) {
- themeStore.featureFlags[skipUnknownPackWarningFeatureFlag] = true
+ appSettings.featureFlags[skipUnknownPackWarningFeatureFlag] = true
const settings = await getSettings()
settings.feature_flags[skipUnknownPackWarningFeatureFlag] = true
await setSettings(settings)
diff --git a/apps/app-frontend/src/components/ui/library/instance-group/instance-card.vue b/apps/app-frontend/src/components/ui/library/instance-group/instance-card.vue
index 814cab72b4..49e2a15b36 100644
--- a/apps/app-frontend/src/components/ui/library/instance-group/instance-card.vue
+++ b/apps/app-frontend/src/components/ui/library/instance-group/instance-card.vue
@@ -9,13 +9,13 @@ import { useRouter } from 'vue-router'
import InstanceCardView from '@/components/ui/library/instance-group/instance-card-view.vue'
import { getLibraryInstanceSelectionKey, useLibrary } from '@/components/ui/library/use-library'
import { useAppEvent } from '@/composables/use-app-event'
+import { handleSevereError } from '@/composables/use-error.js'
import { trackEvent } from '@/helpers/analytics'
import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install'
import { kill, run } from '@/helpers/instance'
import { get_by_instance_id } from '@/helpers/process'
import type { GameInstance } from '@/helpers/types'
import { showInstanceInFolder } from '@/helpers/utils.js'
-import { handleSevereError } from '@/store/error.js'
type ProcessEvent = 'installing' | 'launched' | 'finished'
diff --git a/apps/app-frontend/src/components/ui/library/library-toolbar/sort-menu.vue b/apps/app-frontend/src/components/ui/library/library-toolbar/sort-menu.vue
index c24b5e26ba..457800e1ff 100644
--- a/apps/app-frontend/src/components/ui/library/library-toolbar/sort-menu.vue
+++ b/apps/app-frontend/src/components/ui/library/library-toolbar/sort-menu.vue
@@ -15,6 +15,8 @@ const { formatMessage } = useVIntl()
const messages = defineMessages({
name: { id: 'app.library.sort.name', defaultMessage: 'Name' },
+ loaderSort: { id: 'app.library.sort.loader', defaultMessage: 'Loader' },
+ gameVersionSort: { id: 'app.library.sort.game-version', defaultMessage: 'Game version' },
lastPlayed: { id: 'app.library.sort.last-played', defaultMessage: 'Last played' },
hoursPlayed: { id: 'app.library.sort.hours-played', defaultMessage: 'Hours played' },
dateCreated: { id: 'app.library.sort.date-created', defaultMessage: 'Date created' },
@@ -34,6 +36,8 @@ const sortLabels = {
'Hours played': messages.hoursPlayed,
'Date created': messages.dateCreated,
'Date modified': messages.dateModified,
+ Loader: messages.loaderSort,
+ 'Game version': messages.gameVersionSort,
}
const groupLabels = {
@@ -60,6 +64,7 @@ const groupOptions: ComboboxOption
[] = libraryGroupOptions.map((
class="w-max"
:options="sortOptions"
:show-icon-in-selected="false"
+ :max-height="320"
dropdown-min-width="160px"
>
diff --git a/apps/app-frontend/src/components/ui/library/use-library.ts b/apps/app-frontend/src/components/ui/library/use-library.ts
index 587c6a6c8a..af50ea32f7 100644
--- a/apps/app-frontend/src/components/ui/library/use-library.ts
+++ b/apps/app-frontend/src/components/ui/library/use-library.ts
@@ -36,6 +36,8 @@ export const librarySortOptions = [
'Hours played',
'Date created',
'Date modified',
+ 'Loader',
+ 'Game version',
] as const
export const libraryGroupOptions = [
@@ -328,6 +330,18 @@ function createLibraryState(instances: Ref) {
case 'Name':
visibleInstances.sort((a, b) => a.name.localeCompare(b.name))
break
+ case 'Loader':
+ visibleInstances.sort((a, b) =>
+ formatLoader(formatMessage, a.loader).localeCompare(
+ formatLoader(formatMessage, b.loader),
+ ),
+ )
+ break
+ case 'Game version':
+ visibleInstances.sort((a, b) =>
+ a.game_version.localeCompare(b.game_version, undefined, { numeric: true }),
+ )
+ break
case 'Last played':
visibleInstances.sort((a, b) => dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0)))
break
diff --git a/apps/app-frontend/src/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue b/apps/app-frontend/src/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue
index 9f29f9a945..8d34f9e1f8 100644
--- a/apps/app-frontend/src/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue
+++ b/apps/app-frontend/src/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue
@@ -10,8 +10,8 @@ import {
import { Admonition, Button, ButtonLink, Collapsible, IconButton, NewModal } from '@modrinth/ui'
import { computed, ref } from 'vue'
+import { handleSevereError } from '@/composables/use-error.js'
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
-import { handleSevereError } from '@/store/error.js'
import { findMinecraftAuthError, type MinecraftAuthError } from './minecraft-auth-errors'
diff --git a/apps/app-frontend/src/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue b/apps/app-frontend/src/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue
index c22b268e2a..73293f5bed 100644
--- a/apps/app-frontend/src/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue
+++ b/apps/app-frontend/src/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue
@@ -63,9 +63,9 @@ import { inject, type Ref, ref } from 'vue'
import steveImage from '@/assets/steve-look-up-left.webp'
import type AccountsCard from '@/components/ui/AccountsCard.vue'
+import { handleSevereError } from '@/composables/use-error.js'
import { trackEvent } from '@/helpers/analytics'
import { login as loginFlow, set_default_user } from '@/helpers/auth.js'
-import { handleSevereError } from '@/store/error.js'
const { formatMessage } = useVIntl()
const accountsCard = inject('accountsCard') as Ref | null>
diff --git a/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue b/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue
index 90f4b25499..97258665d3 100644
--- a/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue
+++ b/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue
@@ -36,16 +36,16 @@ import LanguageSettings from '@/components/ui/settings/display/LanguageSettings.
import DefaultInstanceSettings from '@/components/ui/settings/instances/DefaultInstanceSettings.vue'
import JavaSettings from '@/components/ui/settings/instances/JavaSettings.vue'
import ResourceManagementSettings from '@/components/ui/settings/instances/ResourceManagementSettings.vue'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
import { get, set } from '@/helpers/settings.ts'
import {
appSettingsModalContextKey,
type UnsavedChangesController,
} from '@/providers/app-settings-modal'
import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts'
-import { useTheming } from '@/store/state'
// TODO: Apply COMPONENT_STRUCTURE.md here and extract out common setting option components
-const themeStore = useTheming()
+const appSettings = useAppSettings()
const { formatMessage } = useVIntl()
@@ -157,7 +157,9 @@ const tabs = [
},
]
-const availableTabs = computed(() => tabs.filter((tab) => !tab.developerOnly || themeStore.devMode))
+const availableTabs = computed(() =>
+ tabs.filter((tab) => !tab.developerOnly || appSettings.devMode),
+)
const modal = ref | null>(null)
const unsavedChangesPopup = ref<{ nudge: () => void } | null>(null)
@@ -170,10 +172,19 @@ const modifiedUnsavedChangesState = computed(
() => unsavedChangesController.value?.getModified() ?? emptyUnsavedChangesState,
)
const savingUnsavedChanges = computed(() => unsavedChangesController.value?.isSaving() ?? false)
-const hasUnsavedChanges = computed(() => unsavedChangesController.value?.hasChanges() ?? false)
+const hasUnsavedChanges = computed(
+ () =>
+ (unsavedChangesController.value?.hasChanges() ?? false) ||
+ (unsavedChangesController.value?.isSaving() ?? false),
+)
function canLeaveCurrentTab(): boolean {
- if (!unsavedChangesController.value?.hasChanges()) return true
+ if (
+ !unsavedChangesController.value?.hasChanges() &&
+ !unsavedChangesController.value?.isSaving()
+ ) {
+ return true
+ }
unsavedChangesPopup.value?.nudge()
return false
}
@@ -233,8 +244,8 @@ function devModeCount() {
if (devModeCounter.value > 5) {
const selectedTab = modal.value ? availableTabs.value[modal.value.selectedTab] : undefined
- themeStore.devMode = !themeStore.devMode
- settings.value.developer_mode = !!themeStore.devMode
+ appSettings.devMode = !appSettings.devMode
+ settings.value.developer_mode = !!appSettings.devMode
devModeCounter.value = 0
if (modal.value) {
@@ -298,7 +309,7 @@ const messages = defineMessages({
-
+
{{ formatMessage(developerModeEnabled) }}
@@ -306,8 +317,8 @@ const messages = defineMessages({
:aria-label="formatMessage(messages.developerModeButtonLabel)"
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
:class="{
- 'text-brand': themeStore.devMode,
- 'text-secondary': !themeStore.devMode,
+ 'text-brand': appSettings.devMode,
+ 'text-secondary': !appSettings.devMode,
}"
@click="devModeCount"
>
diff --git a/apps/app-frontend/src/components/ui/settings/account/SocialSettings.vue b/apps/app-frontend/src/components/ui/settings/account/SocialSettings.vue
index 15bb622c17..f74eae6598 100644
--- a/apps/app-frontend/src/components/ui/settings/account/SocialSettings.vue
+++ b/apps/app-frontend/src/components/ui/settings/account/SocialSettings.vue
@@ -1,5 +1,6 @@
import type { Labrinth } from '@modrinth/api-client'
import { AccountSocialSettings, injectModrinthClient } from '@modrinth/ui'
+import { inject, onBeforeUnmount, onMounted, ref } from 'vue'
import { get_blocked_users, unblock_user } from '@/helpers/users'
+import { appSettingsModalContextKey } from '@/providers/app-settings-modal'
const client = injectModrinthClient()
+const settingsModal = inject(appSettingsModalContextKey, null)
+const socialSettings = ref | null>(null)
+
+onMounted(() => {
+ settingsModal?.registerUnsavedChangesController({
+ hasChanges: () => socialSettings.value?.hasChanges ?? false,
+ getOriginal: () => socialSettings.value?.originalState ?? {},
+ getModified: () => socialSettings.value?.modifiedState ?? {},
+ isSaving: () => socialSettings.value?.saving ?? false,
+ reset: () => socialSettings.value?.reset(),
+ save: () => socialSettings.value?.save(),
+ })
+})
+
+onBeforeUnmount(() => {
+ settingsModal?.registerUnsavedChangesController(null)
+})
function getUsers(userIds: string[]): Promise {
return client.labrinth.users_v2.getMultiple(userIds)
diff --git a/apps/app-frontend/src/components/ui/settings/display/AppearanceSettings.vue b/apps/app-frontend/src/components/ui/settings/display/AppearanceSettings.vue
index 19acfad5ad..28fe08d0af 100644
--- a/apps/app-frontend/src/components/ui/settings/display/AppearanceSettings.vue
+++ b/apps/app-frontend/src/components/ui/settings/display/AppearanceSettings.vue
@@ -1,108 +1,154 @@
-
-
- {{ formatMessage(messages.colorThemeTitle) }}
-
- {{ formatMessage(messages.colorThemeDescription) }}
+async function saveAppearanceSettings(): Promise {
+ try {
+ await save()
+ } catch {
+ return
+ }
+}
-
+onMounted(() => {
+ settingsModal?.registerUnsavedChangesController({
+ hasChanges: () => hasChanges.value,
+ getOriginal: () => saved.value,
+ getModified: () => changes.value,
+ isSaving: () => saving.value,
+ reset,
+ save: saveAppearanceSettings,
+ })
+})
-
-
-
- {{ formatMessage(messages.advancedRenderingTitle) }}
-
-
- {{ formatMessage(messages.advancedRenderingDescription) }}
-
-
+onBeforeUnmount(() => {
+ theme.preview = null
+ settingsModal?.registerUnsavedChangesController(null)
+})
-
{
- themeStore.advancedRendering = !!e
- settings.advanced_rendering = themeStore.advancedRendering
+provideAppearanceSettings({
+ deferPersistence: true,
+ theme: {
+ current: computed(() => current.value.theme),
+ options: themeOptions,
+ system: computed(() => theme.native),
+ set: setTheme,
+ syncAcrossDevices: {
+ value: computed(() => current.value.syncAcrossDevices),
+ set: setSyncAcrossDevices,
+ },
+ syncDisabled: computed(() => !auth.user.value),
+ },
+ advancedRendering: {
+ value: computed(() => current.value.advancedRendering),
+ set: setAdvancedRendering,
+ },
+ nativeDecorations:
+ os !== 'MacOS'
+ ? {
+ value: computed(() => current.value.nativeDecorations),
+ set: setNativeDecorations,
}
- "
- />
-
+ : undefined,
+ updatePreferences,
+})
+
-
-
-
- {{ formatMessage(messages.nativeDecorationsTitle) }}
-
-
{{ formatMessage(messages.nativeDecorationsDescription) }}
-
-
-
+
+
diff --git a/apps/app-frontend/src/components/ui/settings/display/BehaviorSettings.vue b/apps/app-frontend/src/components/ui/settings/display/BehaviorSettings.vue
index 80de14a718..06a8b3901d 100644
--- a/apps/app-frontend/src/components/ui/settings/display/BehaviorSettings.vue
+++ b/apps/app-frontend/src/components/ui/settings/display/BehaviorSettings.vue
@@ -1,13 +1,27 @@
-
+
+
+
+
+ {{ formatMessage(messages.syncAcrossDevicesTitle) }}
+
+
+ {{ formatMessage(messages.syncAcrossDevicesDescription) }}
+
+
+
+
+
+
+
+
+
{{ formatMessage(messages.startupAndNavigationTitle) }}
@@ -121,7 +266,7 @@ watch(
{{ formatMessage(messages.minimizeLauncherDescription) }}
-
+
@@ -131,16 +276,7 @@ watch(
{{ formatMessage(messages.toggleSidebarDescription) }}
-
@@ -159,17 +295,7 @@ watch(
{{ formatMessage(messages.jumpBackIntoWorldsDescription) }}
- {
- const newValue = !themeStore.getFeatureFlag(worldsInHomeFlag)
- themeStore.featureFlags[worldsInHomeFlag] = newValue
- settings.feature_flags[worldsInHomeFlag] = newValue
- }
- "
- />
+
@@ -179,17 +305,7 @@ watch(
{{ formatMessage(messages.showPlayTimeDescription) }}
- {
- const newValue = !themeStore.getFeatureFlag(showPlayTimeFlag)
- themeStore.featureFlags[showPlayTimeFlag] = newValue
- settings.feature_flags[showPlayTimeFlag] = newValue
- }
- "
- />
+
@@ -199,16 +315,7 @@ watch(
{{ formatMessage(messages.hideNametagDescription) }}
- {
- themeStore.hideNametagSkinsPage = !!e
- settings.hide_nametag_skins_page = themeStore.hideNametagSkinsPage
- }
- "
- />
+
@@ -229,15 +336,7 @@ watch(
{
- const warnBeforeUnknownPackInstall = !!e
- const skipUnknownPackWarning = !warnBeforeUnknownPackInstall
- themeStore.featureFlags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
- settings.feature_flags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
- }
- "
+ v-model="current.warnOnUnknownModpacks"
/>
@@ -250,17 +349,7 @@ watch(
{{ formatMessage(messages.skipNonEssentialWarningsDescription) }}
- {
- const newValue = !themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)
- themeStore.featureFlags[skipNonEssentialWarningsFlag] = newValue
- settings.feature_flags[skipNonEssentialWarningsFlag] = newValue
- }
- "
- />
+
diff --git a/apps/app-frontend/src/components/ui/settings/display/FeatureFlagSettings.vue b/apps/app-frontend/src/components/ui/settings/display/FeatureFlagSettings.vue
index 29f314f140..aa6f0448f8 100644
--- a/apps/app-frontend/src/components/ui/settings/display/FeatureFlagSettings.vue
+++ b/apps/app-frontend/src/components/ui/settings/display/FeatureFlagSettings.vue
@@ -2,17 +2,20 @@
import { Button, Toggle } from '@modrinth/ui'
import { ref, watch } from 'vue'
+import {
+ DEFAULT_FEATURE_FLAGS,
+ type FeatureFlag,
+ useAppSettings,
+} from '@/composables/use-app-settings.ts'
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
-import { useTheming } from '@/store/state'
-import { DEFAULT_FEATURE_FLAGS, type FeatureFlag } from '@/store/theme.ts'
-const themeStore = useTheming()
+const appSettings = useAppSettings()
const settings = ref(await getSettings())
-const options = ref(Object.keys(DEFAULT_FEATURE_FLAGS))
+const options = ref(Object.keys(DEFAULT_FEATURE_FLAGS) as FeatureFlag[])
-function setFeatureFlag(key: string, value: boolean) {
- themeStore.featureFlags[key] = value
+function setFeatureFlag(key: FeatureFlag, value: boolean) {
+ appSettings.featureFlags[key] = value
settings.value.feature_flags[key] = value
}
@@ -35,15 +38,15 @@ watch(
setFeatureFlag(option, !themeStore.getFeatureFlag(option))"
+ :model-value="appSettings.getFeatureFlag(option)"
+ @update:model-value="() => setFeatureFlag(option, !appSettings.getFeatureFlag(option))"
/>
diff --git a/apps/app-frontend/src/components/ui/settings/display/LanguageSettings.vue b/apps/app-frontend/src/components/ui/settings/display/LanguageSettings.vue
index 01f4384bcc..0083a46e7a 100644
--- a/apps/app-frontend/src/components/ui/settings/display/LanguageSettings.vue
+++ b/apps/app-frontend/src/components/ui/settings/display/LanguageSettings.vue
@@ -1,74 +1,35 @@
+
+
+
+
-
-
-
- {{ formatMessage(commonSettingsMessages.language) }}
-
-
-
- {{ formatMessage(languageSelectorMessages.languageWarning, { platform }) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/app-frontend/src/components/ui/settings/instances/ResourceManagementSettings.vue b/apps/app-frontend/src/components/ui/settings/instances/ResourceManagementSettings.vue
index 0fc1a131f6..55d6404005 100644
--- a/apps/app-frontend/src/components/ui/settings/instances/ResourceManagementSettings.vue
+++ b/apps/app-frontend/src/components/ui/settings/instances/ResourceManagementSettings.vue
@@ -14,14 +14,14 @@ import { open } from '@tauri-apps/plugin-dialog'
import { ref, watch } from 'vue'
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
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 appSettings = useAppSettings()
const settings = ref(await get())
const purgeCacheConfirmModal = ref(null)
const alwaysShowCopyDetailsFlag = 'always_show_copy_details'
@@ -145,7 +145,7 @@ async function purgeCache() {
}
function handlePurgeCacheClick() {
- if (themeStore.getFeatureFlag('skip_non_essential_warnings')) {
+ if (appSettings.getFeatureFlag('skip_non_essential_warnings')) {
void purgeCache()
return
}
@@ -210,11 +210,11 @@ async function findLauncherDir() {
{
- const newValue = !themeStore.getFeatureFlag(alwaysShowCopyDetailsFlag)
- themeStore.featureFlags[alwaysShowCopyDetailsFlag] = newValue
+ const newValue = !appSettings.getFeatureFlag(alwaysShowCopyDetailsFlag)
+ appSettings.featureFlags[alwaysShowCopyDetailsFlag] = newValue
settings.feature_flags[alwaysShowCopyDetailsFlag] = newValue
}
"
diff --git a/apps/app-frontend/src/components/ui/shared-instances/shared-instance-invite-handler/use-shared-instance-invite-handler.ts b/apps/app-frontend/src/components/ui/shared-instances/shared-instance-invite-handler/use-shared-instance-invite-handler.ts
index 7135fa99df..00bd70b9c0 100644
--- a/apps/app-frontend/src/components/ui/shared-instances/shared-instance-invite-handler/use-shared-instance-invite-handler.ts
+++ b/apps/app-frontend/src/components/ui/shared-instances/shared-instance-invite-handler/use-shared-instance-invite-handler.ts
@@ -9,6 +9,7 @@ import { useQueryClient } from '@tanstack/vue-query'
import { type Ref, watch } from 'vue'
import { useRouter } from 'vue-router'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
import { get_user } from '@/helpers/cache'
import { toError } from '@/helpers/errors'
import {
@@ -18,7 +19,6 @@ import {
} from '@/helpers/install'
import { list } from '@/helpers/instance'
import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
-import { useTheming } from '@/store/state'
import { parseSharedInstanceInviteNotification } from './shared-instance-invite-parser'
import type { AppNotification, SharedInstanceInvite } from './shared-instance-invite-types'
@@ -58,7 +58,7 @@ export function useSharedInstanceInviteHandler(
const popupNotificationManager = injectPopupNotificationManager()
const queryClient = useQueryClient()
const router = useRouter()
- const themeStore = useTheming()
+ const appSettings = useAppSettings()
const displayedNotifications = new Set()
const displayedNotificationKeys = new Set()
const popupNotificationIds = new Set()
@@ -121,7 +121,7 @@ export function useSharedInstanceInviteHandler(
(instance) => instance.shared_instance?.id === sharedInstanceId,
)
- if (!existingInstance || themeStore.getFeatureFlag('skip_non_essential_warnings')) {
+ if (!existingInstance || appSettings.getFeatureFlag('skip_non_essential_warnings')) {
showInstall(preview, install, creator)
return
}
diff --git a/apps/app-frontend/src/components/ui/world/InstanceItem.vue b/apps/app-frontend/src/components/ui/world/InstanceItem.vue
index 4dc5bd4850..f922cc7809 100644
--- a/apps/app-frontend/src/components/ui/world/InstanceItem.vue
+++ b/apps/app-frontend/src/components/ui/world/InstanceItem.vue
@@ -28,12 +28,12 @@ import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAppEvent } from '@/composables/use-app-event'
+import { handleSevereError } from '@/composables/use-error.js'
import { trackEvent } from '@/helpers/analytics'
import { getInstanceIconUrl, kill, run } from '@/helpers/instance'
import { get_by_instance_id } from '@/helpers/process'
import type { GameInstance } from '@/helpers/types'
import { showInstanceInFolder } from '@/helpers/utils'
-import { handleSevereError } from '@/store/error'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
diff --git a/apps/app-frontend/src/components/ui/world/RecentWorldsList.vue b/apps/app-frontend/src/components/ui/world/RecentWorldsList.vue
index bf6c0d84d8..6616db088c 100644
--- a/apps/app-frontend/src/components/ui/world/RecentWorldsList.vue
+++ b/apps/app-frontend/src/components/ui/world/RecentWorldsList.vue
@@ -10,6 +10,8 @@ import { computed, onMounted, ref, watch } from 'vue'
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
import WorldItem from '@/components/ui/world/WorldItem.vue'
import { useAppEvent } from '@/composables/use-app-event'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
+import { handleSevereError } from '@/composables/use-error.js'
import { trackEvent } from '@/helpers/analytics'
import { kill, run } from '@/helpers/instance'
import { get_all } from '@/helpers/process'
@@ -29,8 +31,6 @@ import {
start_join_singleplayer_world,
type WorldWithInstance,
} from '@/helpers/worlds.ts'
-import { handleSevereError } from '@/store/error'
-import { useTheming } from '@/store/theme.ts'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
@@ -42,7 +42,7 @@ const props = defineProps<{
recentInstances: GameInstance[]
}>()
-const theme = useTheming()
+const appSettings = useAppSettings()
const jumpBackInItems = ref([])
const loading = ref(true)
@@ -76,7 +76,7 @@ type WorldJumpBackInItem = BaseJumpBackInItem & {
type JumpBackInItem = InstanceJumpBackInItem | WorldJumpBackInItem
-const showWorlds = computed(() => theme.getFeatureFlag('worlds_in_home'))
+const showWorlds = computed(() => appSettings.getFeatureFlag('worlds_in_home'))
watch([() => props.recentInstances, () => showWorlds.value], async () => {
await populateJumpBackIn().catch(() => {
diff --git a/apps/app-frontend/src/composables/browse/install-job-notifications.ts b/apps/app-frontend/src/composables/browse/install-job-notifications.ts
index 7572434d36..c45a61cfff 100644
--- a/apps/app-frontend/src/composables/browse/install-job-notifications.ts
+++ b/apps/app-frontend/src/composables/browse/install-job-notifications.ts
@@ -10,6 +10,7 @@ import { convertFileSrc } from '@tauri-apps/api/core'
import { computed, ref } from 'vue'
import type { Router } from 'vue-router'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
import {
install_job_dismiss,
install_job_list,
@@ -23,7 +24,6 @@ import {
} from '@/helpers/install'
import { get_many as getInstances } from '@/helpers/instance'
import { injectAppEvents } from '@/providers/app-events'
-import { useTheming } from '@/store/state'
const messages = defineMessages({
installs: {
@@ -236,7 +236,7 @@ export async function useInstallJobNotifications(opts: {
}) {
const appEvents = injectAppEvents()
const { formatMessage } = useVIntl()
- const themeStore = useTheming()
+ const appSettings = useAppSettings()
const jobs = ref([])
const iconUrls = ref>({})
const instanceNames = ref>({})
@@ -421,7 +421,7 @@ export async function useInstallJobNotifications(opts: {
}
function shouldShowCopyDetails(job: InstallJobSnapshot): boolean {
- return isTerminalJob(job) || themeStore.getFeatureFlag('always_show_copy_details')
+ return isTerminalJob(job) || appSettings.getFeatureFlag('always_show_copy_details')
}
function isCopied(job: InstallJobSnapshot): boolean {
diff --git a/apps/app-frontend/src/composables/use-app-settings.ts b/apps/app-frontend/src/composables/use-app-settings.ts
new file mode 100644
index 0000000000..4ef0a51609
--- /dev/null
+++ b/apps/app-frontend/src/composables/use-app-settings.ts
@@ -0,0 +1,52 @@
+import { reactive, ref } from 'vue'
+
+export const DEFAULT_FEATURE_FLAGS = {
+ project_background: false,
+ page_path: false,
+ worlds_in_home: true,
+ server_project_qa: false,
+ show_version_environment_column: false,
+ server_ram_as_bytes_always_on: false,
+ always_show_app_controls: false,
+ skip_non_essential_warnings: false,
+ skip_unknown_pack_warning: false,
+ pride_fundraiser: true,
+ i18n_debug: false,
+ show_instance_play_time: true,
+ advanced_filters_collapsed: true,
+ always_show_copy_details: false,
+ hide_installed_modpacks: false,
+ friends_active_collapsed: false,
+ friends_online_collapsed: false,
+ friends_offline_collapsed: true,
+ friends_pending_collapsed: true,
+ dismissed_photosensitivity_filter_warning: false,
+}
+
+export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
+type FeatureFlags = Record
+
+const syncBehaviorAcrossDevices = ref(false)
+const featureFlags = reactive({ ...DEFAULT_FEATURE_FLAGS })
+
+function setBehaviorSyncAcrossDevices(enabled: boolean): void {
+ syncBehaviorAcrossDevices.value = enabled
+}
+
+function getFeatureFlag(key: FeatureFlag): boolean {
+ return featureFlags[key] ?? DEFAULT_FEATURE_FLAGS[key]
+}
+
+const appSettings = reactive({
+ syncBehaviorAcrossDevices,
+ hideNametagSkinsPage: false,
+ toggleSidebar: false,
+ devMode: false,
+ featureFlags,
+ setBehaviorSyncAcrossDevices,
+ getFeatureFlag,
+})
+
+export function useAppSettings() {
+ return appSettings
+}
diff --git a/apps/app-frontend/src/composables/use-error.js b/apps/app-frontend/src/composables/use-error.js
new file mode 100644
index 0000000000..8323630733
--- /dev/null
+++ b/apps/app-frontend/src/composables/use-error.js
@@ -0,0 +1,48 @@
+import { reactive } from 'vue'
+
+import { findMinecraftAuthError } from '@/components/ui/minecraft-auth-error-modal/minecraft-auth-errors'
+
+const errorState = reactive({
+ errorModal: null,
+ minecraftAuthErrorModal: null,
+ minecraftRequiredModal: null,
+ setErrorModal(ref) {
+ this.errorModal = ref
+ },
+ setMinecraftAuthErrorModal(ref) {
+ this.minecraftAuthErrorModal = ref
+ },
+ setMinecraftRequiredModal(ref) {
+ this.minecraftRequiredModal = ref
+ },
+ showError(error, context, closable = true, source = null) {
+ const errorMessage = error.message?.toLowerCase()
+ if (
+ (errorMessage?.includes('user is not logged in') ||
+ errorMessage?.includes('cannot play instance since minecraft is required')) &&
+ this.minecraftRequiredModal
+ ) {
+ this.minecraftRequiredModal.show()
+ return
+ }
+ if (
+ error.message &&
+ (error.message.includes('Minecraft authentication error:') ||
+ findMinecraftAuthError(error.message)) &&
+ this.minecraftAuthErrorModal
+ ) {
+ this.minecraftAuthErrorModal.show(error)
+ return
+ }
+ this.errorModal.show(error, context, closable, source)
+ },
+})
+
+export function useError() {
+ return errorState
+}
+
+export const handleSevereError = (error, context) => {
+ useError().showError(error, context)
+ console.error(error)
+}
diff --git a/apps/app-frontend/src/composables/use-theme.ts b/apps/app-frontend/src/composables/use-theme.ts
new file mode 100644
index 0000000000..8a5aff4263
--- /dev/null
+++ b/apps/app-frontend/src/composables/use-theme.ts
@@ -0,0 +1,47 @@
+import { computed, reactive, ref, watch } from 'vue'
+
+export const THEME_OPTIONS = ['dark', 'light', 'oled', 'retro', 'system'] as const
+
+export type ColorTheme = (typeof THEME_OPTIONS)[number]
+type Theme = Exclude
+
+const preferred = ref('dark')
+const preview = ref(null)
+const advancedRendering = ref(true)
+const syncAcrossDevices = ref(false)
+const nativeThemeQuery = window.matchMedia('(prefers-color-scheme: dark)')
+const native = ref(nativeThemeQuery.matches ? 'dark' : 'light')
+const active = computed(() => {
+ const selectedTheme = preview.value ?? preferred.value
+ return selectedTheme === 'system' ? native.value : selectedTheme
+})
+
+nativeThemeQuery.addEventListener('change', (event) => {
+ native.value = event.matches ? 'dark' : 'light'
+})
+
+watch(
+ active,
+ (theme) => {
+ const html = document.documentElement
+ for (const option of THEME_OPTIONS) {
+ html.classList.remove(`${option}-mode`)
+ }
+ html.classList.add(`${theme}-mode`)
+ },
+ { immediate: true },
+)
+
+const theme = reactive({
+ preferred,
+ preview,
+ active,
+ native,
+ syncAcrossDevices,
+ advancedRendering,
+ options: THEME_OPTIONS,
+})
+
+export function useTheme() {
+ return theme
+}
diff --git a/apps/app-frontend/src/helpers/settings.ts b/apps/app-frontend/src/helpers/settings.ts
index 238d4116e2..340ce8a498 100644
--- a/apps/app-frontend/src/helpers/settings.ts
+++ b/apps/app-frontend/src/helpers/settings.ts
@@ -5,8 +5,9 @@
*/
import { invoke } from '@tauri-apps/api/core'
+import type { FeatureFlag } from '@/composables/use-app-settings.ts'
+import type { ColorTheme } from '@/composables/use-theme.ts'
import type { Hooks, MemorySettings, WindowSize } from '@/helpers/types'
-import type { ColorTheme, FeatureFlag } from '@/store/theme.ts'
// Settings object
/*
@@ -43,6 +44,8 @@ export type AppSettings = {
advanced_rendering: boolean
native_decorations: boolean
toggle_sidebar: boolean
+ sync_theme_across_devices: boolean
+ sync_behavior_across_devices: boolean
telemetry: boolean
discord_rpc: boolean
diff --git a/apps/app-frontend/src/helpers/types.d.ts b/apps/app-frontend/src/helpers/types.d.ts
index becbda9208..5981a51f6e 100644
--- a/apps/app-frontend/src/helpers/types.d.ts
+++ b/apps/app-frontend/src/helpers/types.d.ts
@@ -213,6 +213,8 @@ type AppSettings = {
advanced_rendering: boolean
native_decorations: boolean
worlds_in_home: boolean
+ sync_theme_across_devices: boolean
+ sync_behavior_across_devices: boolean
telemetry: boolean
discord_rpc: boolean
diff --git a/apps/app-frontend/src/helpers/user-preferences.ts b/apps/app-frontend/src/helpers/user-preferences.ts
new file mode 100644
index 0000000000..32261626a8
--- /dev/null
+++ b/apps/app-frontend/src/helpers/user-preferences.ts
@@ -0,0 +1,19 @@
+import type { UserPreferencesContext } from '@modrinth/ui'
+import { invoke } from '@tauri-apps/api/core'
+
+type UserPreferences = NonNullable
+type PartialUserPreferences = Parameters[0]
+
+export async function get_user_preferences(userId: string): Promise {
+ return await invoke('plugin:users|get_user_preferences', { userId })
+}
+
+export async function patch_user_preferences(
+ userId: string,
+ preferences: PartialUserPreferences,
+): Promise {
+ return await invoke('plugin:users|patch_user_preferences', {
+ userId,
+ preferences,
+ })
+}
diff --git a/apps/app-frontend/src/locales/ar-SA/index.json b/apps/app-frontend/src/locales/ar-SA/index.json
index c85293fc36..c622a6f4a8 100644
--- a/apps/app-frontend/src/locales/ar-SA/index.json
+++ b/apps/app-frontend/src/locales/ar-SA/index.json
@@ -134,12 +134,6 @@
"app.ads-consent.reject": {
"message": "رفض الكل"
},
- "app.appearance-settings.advanced-rendering.title": {
- "message": "العرض المتقدم"
- },
- "app.appearance-settings.color-theme.title": {
- "message": "لون السمة"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "صفحة الرئيسية"
},
@@ -161,12 +155,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "تصغير نافذة المشغل"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "استخدام إطار نافذة النظام (يتطلب إعادة تشغيل التطبيق)."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "الزخارف التقليدية"
- },
"app.appearance-settings.show-play-time.description": {
"message": "يعرض مقدار الوقت الذي قضيته في لعب إحدى المهام."
},
diff --git a/apps/app-frontend/src/locales/cs-CZ/index.json b/apps/app-frontend/src/locales/cs-CZ/index.json
index 58476dde1a..e0feddb4e9 100644
--- a/apps/app-frontend/src/locales/cs-CZ/index.json
+++ b/apps/app-frontend/src/locales/cs-CZ/index.json
@@ -74,12 +74,6 @@
"app.ads-consent.title": {
"message": "Vaše soukromí a způsob, jakým reklamy podporují Modrinth"
},
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Pokročilé vykreslování"
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Barevný motiv"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Domů"
},
@@ -101,12 +95,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimalizovat launcher"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Použít systémový rámeček okna (vyžaduje restart aplikace)."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Systémové dekorace"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Zobrazuje, kolik času jsi strávil hraním instance."
},
diff --git a/apps/app-frontend/src/locales/da-DK/index.json b/apps/app-frontend/src/locales/da-DK/index.json
index 548f236198..24f14fca71 100644
--- a/apps/app-frontend/src/locales/da-DK/index.json
+++ b/apps/app-frontend/src/locales/da-DK/index.json
@@ -137,12 +137,6 @@
"app.ads-consent.title": {
"message": "Dit privatliv og hvordan reklamer hjælper Modrinth"
},
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Avanceret rendering"
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Farvetema"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Hjem"
},
diff --git a/apps/app-frontend/src/locales/de-CH/index.json b/apps/app-frontend/src/locales/de-CH/index.json
index e55bad3b8d..a773966a46 100644
--- a/apps/app-frontend/src/locales/de-CH/index.json
+++ b/apps/app-frontend/src/locales/de-CH/index.json
@@ -143,18 +143,6 @@
"app.ads-consent.title": {
"message": "Deine Privatsphäre und wie Werbung Modrinth unterstützt"
},
- "app.appearance-settings.advanced-rendering.description": {
- "message": "Visuelle Effekte wie Hintergrundunschärfe aktivieren. Ohne Hardwarebeschleunigung kann dies die Leistung beeinträchtigen."
- },
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Erweitertes Rendering"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "Wähle das Farbschema der Modrinth App aus."
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Farbschema"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Start"
},
@@ -179,12 +167,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "App minimieren"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Nutzt die Titelleiste und Fenstersteuerung deines Betriebssystems. Erfordert einen App-Neustart."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "System-Fensterrahmen"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Zeigt, wie lange du jede einzelne Instanz gespielt hast."
},
diff --git a/apps/app-frontend/src/locales/de-DE/index.json b/apps/app-frontend/src/locales/de-DE/index.json
index 043b4e6eb8..8f4d7d6f13 100644
--- a/apps/app-frontend/src/locales/de-DE/index.json
+++ b/apps/app-frontend/src/locales/de-DE/index.json
@@ -143,18 +143,6 @@
"app.ads-consent.title": {
"message": "Deine Privatsphäre und wie Werbung Modrinth unterstützt"
},
- "app.appearance-settings.advanced-rendering.description": {
- "message": "Visuelle Effekte wie Hintergrundunschärfe aktivieren. Ohne Hardwarebeschleunigung kann dies die Leistung beeinträchtigen."
- },
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Erweitertes Rendering"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "Wähle das Farbschema der Modrinth App aus."
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Farbschema"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Start"
},
@@ -179,12 +167,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "App minimieren"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Nutzt die Titelleiste und Fenstersteuerung deines Betriebssystems. Erfordert einen App-Neustart."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "System-Fensterrahmen"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Zeigt, wie lange du jede einzelne Instanz gespielt hast."
},
diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json
index e366aa6558..2753c00a8d 100644
--- a/apps/app-frontend/src/locales/en-US/index.json
+++ b/apps/app-frontend/src/locales/en-US/index.json
@@ -149,18 +149,6 @@
"app.ads-consent.title": {
"message": "Your privacy and how ads support Modrinth"
},
- "app.appearance-settings.advanced-rendering.description": {
- "message": "Enable visual effects such as background blur. This may reduce performance without hardware acceleration."
- },
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Advanced rendering"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "Choose the color theme used by Modrinth App."
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Color theme"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Home"
},
@@ -185,12 +173,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimize app"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Use your operating system's title bar and window controls. Requires an app restart."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "System window frame"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Show how long you've played each instance."
},
@@ -230,6 +212,15 @@
"app.behavior-settings.startup-and-navigation.title": {
"message": "Startup and navigation"
},
+ "app.behavior-settings.sync-across-devices.description": {
+ "message": "Use these behavior settings everywhere you're signed in. Turn this off to keep separate settings on this device."
+ },
+ "app.behavior-settings.sync-across-devices.signed-out-tooltip": {
+ "message": "Sign into a Modrinth account to sync settings."
+ },
+ "app.behavior-settings.sync-across-devices.title": {
+ "message": "Sync behavior across devices"
+ },
"app.browse.add-servers-to-instance": {
"message": "Adding server to instance"
},
@@ -1082,6 +1073,9 @@
"app.library.sort.date-modified": {
"message": "Date modified"
},
+ "app.library.sort.game-version": {
+ "message": "Game version"
+ },
"app.library.sort.hours-played": {
"message": "Hours played"
},
@@ -1091,6 +1085,9 @@
"app.library.sort.last-played": {
"message": "Last played"
},
+ "app.library.sort.loader": {
+ "message": "Loader"
+ },
"app.library.sort.name": {
"message": "Name"
},
diff --git a/apps/app-frontend/src/locales/es-419/index.json b/apps/app-frontend/src/locales/es-419/index.json
index 44c5f555c0..4ac1af9b6c 100644
--- a/apps/app-frontend/src/locales/es-419/index.json
+++ b/apps/app-frontend/src/locales/es-419/index.json
@@ -143,18 +143,6 @@
"app.ads-consent.title": {
"message": "Tu privacidad y como los anuncios apoyan a Modrinth"
},
- "app.appearance-settings.advanced-rendering.description": {
- "message": "Activar efectos visuales como los efectos de desenfoque. Esto puede reducir el rendimiento si no tienes aceleración de hardware."
- },
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Renderizado avanzado"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "Escoge el esquema de colores usado por la aplicación de Modrinth."
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Color de la interfaz"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Inicio"
},
@@ -179,12 +167,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimizar app"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Usa el borde de ventana de tu sistema. Necesita reiniciar la aplicación."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Decoraciones nativas"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Muestra cuanto tiempo has jugado en cada instancia."
},
diff --git a/apps/app-frontend/src/locales/es-ES/index.json b/apps/app-frontend/src/locales/es-ES/index.json
index ea50c2a08c..909750303b 100644
--- a/apps/app-frontend/src/locales/es-ES/index.json
+++ b/apps/app-frontend/src/locales/es-ES/index.json
@@ -95,12 +95,6 @@
"app.action-bar.view-logs": {
"message": "Ver registros"
},
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Renderizado avanzado"
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Tema de color"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Inicio"
},
@@ -122,12 +116,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimizar lanzador"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Usa el marco de ventana del sistema (requiere reiniciar la aplicación)."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Decoraciones nativas"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Muestra cuánto tiempo has estado jugando en una instancia."
},
diff --git a/apps/app-frontend/src/locales/fi-FI/index.json b/apps/app-frontend/src/locales/fi-FI/index.json
index 5a614188ce..3193fa981f 100644
--- a/apps/app-frontend/src/locales/fi-FI/index.json
+++ b/apps/app-frontend/src/locales/fi-FI/index.json
@@ -71,12 +71,6 @@
"app.action-bar.view-logs": {
"message": "Katso lokeja"
},
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Edistynyt renderointi"
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Väriteema"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Koti"
},
@@ -98,12 +92,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Piilota käynnistysohjelma"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Käytä järjestelmän ikkuna kehystä (Vaatii sovelluksen uudelleen käynnistämisen)."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Natiivit koristukset"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Näyttää, kuinka paljon aikaa olet käyttänyt instanssin pelaamiseen."
},
diff --git a/apps/app-frontend/src/locales/fil-PH/index.json b/apps/app-frontend/src/locales/fil-PH/index.json
index a0181a28b1..16a0dc43ae 100644
--- a/apps/app-frontend/src/locales/fil-PH/index.json
+++ b/apps/app-frontend/src/locales/fil-PH/index.json
@@ -47,12 +47,6 @@
"app.action-bar.view-logs": {
"message": "Tignan ang logs"
},
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Masalimuot na pagrerender"
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Tema ng kulay"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Tahanan"
},
@@ -74,12 +68,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Liitin ang launcher"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Gamitin ang system window frame (kailangan i-restart ang app)."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Native decorations"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Magpapakita ang katagal ng iyong paglalaro sa isang instansiya."
},
diff --git a/apps/app-frontend/src/locales/fr-FR/index.json b/apps/app-frontend/src/locales/fr-FR/index.json
index 4ffb047de4..f9edddfab1 100644
--- a/apps/app-frontend/src/locales/fr-FR/index.json
+++ b/apps/app-frontend/src/locales/fr-FR/index.json
@@ -143,18 +143,6 @@
"app.ads-consent.title": {
"message": "Votre vie privée et comment les publicités soutiennent Modrinth"
},
- "app.appearance-settings.advanced-rendering.description": {
- "message": "Activer les effets visuels tel que le flou d'arrière-plan. Ceci peut réduire les performances sans accélération matérielle."
- },
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Rendu avancé"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "Choisir la couleur de thème utilisée par Modrinth App."
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Couleur du thème"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Accueil"
},
@@ -179,12 +167,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimiser l'application"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Utilise la barre de titre et les contrôles de fenêtre de votre système d'exploitation. Nécessite le redémarrage de l'application."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Cadre de fenêtre système"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Montre combien de temps vous avez joué à chaque instance."
},
diff --git a/apps/app-frontend/src/locales/hu-HU/index.json b/apps/app-frontend/src/locales/hu-HU/index.json
index e947d24768..fdb6c98d59 100644
--- a/apps/app-frontend/src/locales/hu-HU/index.json
+++ b/apps/app-frontend/src/locales/hu-HU/index.json
@@ -143,18 +143,6 @@
"app.ads-consent.title": {
"message": "Adataid védelme és a hirdetések szerepe a Modrinth működésében"
},
- "app.appearance-settings.advanced-rendering.description": {
- "message": "Engedélyezi a vizuális effektusokat, például a háttér elmosását. Hardveres gyorsítás nélkül ez ronthatja a rendszer teljesítményét."
- },
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Haladó megjelenítési beállítások"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "Válaszd ki a Modrinth App által használt színsémát."
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Téma"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Kezdőlap"
},
@@ -179,12 +167,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Alkalmazás bezárása"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Az operációs rendszer címsorának és ablakvezérlőinek használata. A módosítás érvénybe lépéséhez újra kell indítani az alkalmazást."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Rendszerablakkeret"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Megjeleníti, hogy mennyi időt töltöttél egy példányban."
},
diff --git a/apps/app-frontend/src/locales/id-ID/index.json b/apps/app-frontend/src/locales/id-ID/index.json
index 323d8aa684..cc176e5bdd 100644
--- a/apps/app-frontend/src/locales/id-ID/index.json
+++ b/apps/app-frontend/src/locales/id-ID/index.json
@@ -44,12 +44,6 @@
"app.action-bar.view-logs": {
"message": "Lihat catatan"
},
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Renderasi tingkat lanjut"
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Tema warna"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Beranda"
},
@@ -71,12 +65,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Kecilkan peluncur"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Gunakan bingkai jendela sistem (wajib menjalankan ulang aplikasi)."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Hiasan asli"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Menampilkan seberapa lama Anda memainkan sebuah instans."
},
diff --git a/apps/app-frontend/src/locales/it-IT/index.json b/apps/app-frontend/src/locales/it-IT/index.json
index 57d902ac9c..f53f590846 100644
--- a/apps/app-frontend/src/locales/it-IT/index.json
+++ b/apps/app-frontend/src/locales/it-IT/index.json
@@ -143,18 +143,6 @@
"app.ads-consent.title": {
"message": "La tua privacy e come le pubblicità supportano Modrinth"
},
- "app.appearance-settings.advanced-rendering.description": {
- "message": "Abilita effetti come lo sfondo sfocato. Questo potrebbe ridurre le prestazioni senza accelerazione hardware."
- },
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Rendering avanzato"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "Scegli il tema usato da Modrinth App."
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Tema"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Home"
},
@@ -179,12 +167,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimizza app"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Usa la barra e i controlli delle finestre di sistema. Richiede un riavvio."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Usa cornice di sistema"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Mostra quanto tempo hai giocato a un'istanza."
},
diff --git a/apps/app-frontend/src/locales/ja-JP/index.json b/apps/app-frontend/src/locales/ja-JP/index.json
index b999c783bc..73211145f8 100644
--- a/apps/app-frontend/src/locales/ja-JP/index.json
+++ b/apps/app-frontend/src/locales/ja-JP/index.json
@@ -143,18 +143,6 @@
"app.ads-consent.title": {
"message": "プライバシーと広告がModrinthを支える仕組み"
},
- "app.appearance-settings.advanced-rendering.description": {
- "message": "背景のぼかしなどの視覚効果を有効にします。ハードウェアアクセラレーションが無効の場合、パフォーマンスが低下する可能性があります。"
- },
- "app.appearance-settings.advanced-rendering.title": {
- "message": "高度なレンダリング"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "Modrinth Appのテーマを選びましょう。"
- },
- "app.appearance-settings.color-theme.title": {
- "message": "テーマ"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "ホーム"
},
@@ -179,12 +167,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "ランチャーを最小化"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "OS標準のタイトルバーとウィンドウ操作ボタンを使用します。反映にはアプリの再起動が必要です。"
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "システムウィンドウフレーム"
- },
"app.appearance-settings.show-play-time.description": {
"message": "各インスタンスのプレイ時間を表示します。"
},
diff --git a/apps/app-frontend/src/locales/ko-KR/index.json b/apps/app-frontend/src/locales/ko-KR/index.json
index 525248e3b2..77cefeb25b 100644
--- a/apps/app-frontend/src/locales/ko-KR/index.json
+++ b/apps/app-frontend/src/locales/ko-KR/index.json
@@ -143,18 +143,6 @@
"app.ads-consent.title": {
"message": "개인정보와 광고가 Modrinth를 지원하는 방식"
},
- "app.appearance-settings.advanced-rendering.description": {
- "message": "배경 흐림 효과와 같은 시각 효과를 활성화합니다. 하드웨어 가속이 지원되지 않는 경우 성능이 저하될 수 있습니다."
- },
- "app.appearance-settings.advanced-rendering.title": {
- "message": "고급 렌더링"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "Modrinth 앱에서 사용할 색상 테마를 선택하세요."
- },
- "app.appearance-settings.color-theme.title": {
- "message": "색상 테마"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "홈"
},
@@ -179,12 +167,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "앱 최소화"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "운영 체제의 제목 표시줄 및 창 제어 기능을 사용합니다. 앱 재시작이 필요합니다."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "시스템 창 프레임"
- },
"app.appearance-settings.show-play-time.description": {
"message": "각 인스턴스를 얼마나 플레이했는지 표시합니다."
},
diff --git a/apps/app-frontend/src/locales/ms-MY/index.json b/apps/app-frontend/src/locales/ms-MY/index.json
index ebda1cbf4a..63685a5d4d 100644
--- a/apps/app-frontend/src/locales/ms-MY/index.json
+++ b/apps/app-frontend/src/locales/ms-MY/index.json
@@ -53,12 +53,6 @@
"app.action-bar.view-logs": {
"message": "Lihat log"
},
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Pemaparan lanjutan"
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Tema warna"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Laman utama"
},
@@ -80,12 +74,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimumkan pelancar"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Gunakan bingkai tetingkap sistem (aplikasi perlu dimulakan semula)."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Hiasan asli"
- },
"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."
},
diff --git a/apps/app-frontend/src/locales/nl-NL/index.json b/apps/app-frontend/src/locales/nl-NL/index.json
index fc67b5ff57..d2e18911b0 100644
--- a/apps/app-frontend/src/locales/nl-NL/index.json
+++ b/apps/app-frontend/src/locales/nl-NL/index.json
@@ -143,18 +143,6 @@
"app.ads-consent.title": {
"message": "Je privacy en hoe advertenties Modrinth ondersteunen"
},
- "app.appearance-settings.advanced-rendering.description": {
- "message": "Schakel visuele effecten zoals achtergrondvervaging in. Zonder hardwareversnelling kan dit ten koste gaan van de prestaties."
- },
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Geavanceerde weergave"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "Kies het kleurenthema dat door de Modrinth-app wordt gebruikt."
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Kleurenthema"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Start"
},
@@ -179,12 +167,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "App minimaliseren"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Gebruik de titelbalk en vensterbedieningselementen van je besturingssysteem. Hiervoor moet de app opnieuw worden opgestart."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Systeemvensterframe"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Laat zien hoe lang je elke instantie hebt gespeeld."
},
diff --git a/apps/app-frontend/src/locales/no-NO/index.json b/apps/app-frontend/src/locales/no-NO/index.json
index a12e242ace..d4fbacb96c 100644
--- a/apps/app-frontend/src/locales/no-NO/index.json
+++ b/apps/app-frontend/src/locales/no-NO/index.json
@@ -56,12 +56,6 @@
"app.action-bar.view-logs": {
"message": "Vis logger"
},
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Avansert rendering"
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Fargetema"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Hjem"
},
diff --git a/apps/app-frontend/src/locales/pl-PL/index.json b/apps/app-frontend/src/locales/pl-PL/index.json
index 7e1bd4bae1..6071ed5c49 100644
--- a/apps/app-frontend/src/locales/pl-PL/index.json
+++ b/apps/app-frontend/src/locales/pl-PL/index.json
@@ -143,18 +143,6 @@
"app.ads-consent.title": {
"message": "Twoja prywatność i jak reklamy wspierają Modrinth"
},
- "app.appearance-settings.advanced-rendering.description": {
- "message": "Włącz efekty wizualne, takie jak rozmycie tła. Może to negatywnie wpłynąć na wydajność, jeżeli akceleracja sprzętowa jest wyłączona."
- },
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Zaawansowane renderowanie"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "Wybierz motyw użyty przez Modrinth App."
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Motyw"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Strona główna"
},
@@ -179,12 +167,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimalizuj aplikację"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Używaj pasek tytułu i kontrolki okna systemu operacyjnego. Wymaga ponownego uruchomienia aplikacji."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Systemowa ramka okna"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Pokazuje czas gry spędzony w danej instancji."
},
diff --git a/apps/app-frontend/src/locales/pt-BR/index.json b/apps/app-frontend/src/locales/pt-BR/index.json
index 4d3959f58e..463096efc8 100644
--- a/apps/app-frontend/src/locales/pt-BR/index.json
+++ b/apps/app-frontend/src/locales/pt-BR/index.json
@@ -143,18 +143,6 @@
"app.ads-consent.title": {
"message": "Sua privacidade e como anúncios apoiam o Modrinth"
},
- "app.appearance-settings.advanced-rendering.description": {
- "message": "Ativa os efeitos visuais como o desfoque de fundo. Isso talvez reduza o desempenho sem o aceleramento de hardware."
- },
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Renderização avançada"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "Escolha o tema usado pelo Modrinth App."
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Tema"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Início"
},
@@ -179,12 +167,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimizar aplicativo"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Usa a barra de título e os controles de janela do seu sistema operacional. Exige a reinicialização do aplicativo."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Moldura de janela do sistema"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Exibe o tempo de jogo de cada instância."
},
diff --git a/apps/app-frontend/src/locales/pt-PT/index.json b/apps/app-frontend/src/locales/pt-PT/index.json
index 4e2c6e6f78..daf5c7a0f4 100644
--- a/apps/app-frontend/src/locales/pt-PT/index.json
+++ b/apps/app-frontend/src/locales/pt-PT/index.json
@@ -140,12 +140,6 @@
"app.ads-consent.title": {
"message": "A sua privacidade e como os anúncios são financiados pela Modrinth"
},
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Renderização avançada"
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Cor de tema"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Casa"
},
@@ -167,12 +161,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimizar launcher"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Utilizar a moldura da janela do sistema (é necessário reiniciar a aplicação)."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Decorações nativas"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Apresenta quanto tempo passou a jogar uma instância."
},
diff --git a/apps/app-frontend/src/locales/ru-RU/index.json b/apps/app-frontend/src/locales/ru-RU/index.json
index 6fd2a36fbf..889ee932f0 100644
--- a/apps/app-frontend/src/locales/ru-RU/index.json
+++ b/apps/app-frontend/src/locales/ru-RU/index.json
@@ -143,18 +143,6 @@
"app.ads-consent.title": {
"message": "Конфиденциальность и реклама на Modrinth"
},
- "app.appearance-settings.advanced-rendering.description": {
- "message": "Использовать эффекты вроде размытия фона. Без аппаратного ускорения возможна потеря производительности."
- },
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Расширенные эффекты"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "Выберите тему для Modrinth App."
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Тема"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Главная"
},
@@ -179,12 +167,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Сворачивание приложения"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Использовать системное оформление заголовка и кнопок окна. Требуется перезапуск."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Системная рамка окна"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Показывать проведённое время в сборке."
},
diff --git a/apps/app-frontend/src/locales/sr-CS/index.json b/apps/app-frontend/src/locales/sr-CS/index.json
index f5c67b16a5..b4ed7707da 100644
--- a/apps/app-frontend/src/locales/sr-CS/index.json
+++ b/apps/app-frontend/src/locales/sr-CS/index.json
@@ -143,18 +143,6 @@
"app.ads-consent.title": {
"message": "Tvoja bezbednost i kako reklame pomažu Modrinth-u"
},
- "app.appearance-settings.advanced-rendering.description": {
- "message": "Uključi vizuelne efekte kao što je blur pozadine. Ovo može da pogorša performanse bez hardverskog ubrzanja."
- },
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Napredno renderovanje"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "Izaberi boju teme koju koristi Modrinth App."
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Tema boje"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Početna stranica"
},
@@ -179,12 +167,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Smanji aplikaciju"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Koristi naslovnu traku i kontrole prozora tvog operativnog sistema. Restart aplikacije obavezan."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Sistemski okvir prozora"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Prikaži koliko dugo ste igrali svaku instancu."
},
diff --git a/apps/app-frontend/src/locales/sv-SE/index.json b/apps/app-frontend/src/locales/sv-SE/index.json
index bddb1feab2..d5197c1c09 100644
--- a/apps/app-frontend/src/locales/sv-SE/index.json
+++ b/apps/app-frontend/src/locales/sv-SE/index.json
@@ -140,15 +140,6 @@
"app.ads-consent.title": {
"message": "Din integritet och hur reklam stödjer Modrinth"
},
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Avancerad rendering"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "Välj färgtemat som används av Modrinth App."
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Färgtema"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Hem"
},
@@ -170,12 +161,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimera app"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Använd systemets fönsterram (omstart av app krävs)."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Inbyggda fönsterdekorationer"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Visa hur länge du spelat varje instans."
},
diff --git a/apps/app-frontend/src/locales/th-TH/index.json b/apps/app-frontend/src/locales/th-TH/index.json
index e44beb4a8d..d93134d0c7 100644
--- a/apps/app-frontend/src/locales/th-TH/index.json
+++ b/apps/app-frontend/src/locales/th-TH/index.json
@@ -59,12 +59,6 @@
"app.action-bar.view-logs": {
"message": "ดู Log"
},
- "app.appearance-settings.advanced-rendering.title": {
- "message": "การเรนเดอร์ขั้นสูง"
- },
- "app.appearance-settings.color-theme.title": {
- "message": "ธีมสี"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "หน้าหลัก"
},
@@ -86,12 +80,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "ย่อลันเชอร์"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "ใช้กรอบหน้าต่างระบบ (ต้องรีสตาร์ทแอป)"
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "ของตกแต่งดั้งเดิม"
- },
"app.appearance-settings.show-play-time.description": {
"message": "แสดงเวลาที่คุณใช้เล่นในแต่ละอินสแตนซ์"
},
diff --git a/apps/app-frontend/src/locales/tr-TR/index.json b/apps/app-frontend/src/locales/tr-TR/index.json
index 05080edd1b..15f9078f48 100644
--- a/apps/app-frontend/src/locales/tr-TR/index.json
+++ b/apps/app-frontend/src/locales/tr-TR/index.json
@@ -143,18 +143,6 @@
"app.ads-consent.title": {
"message": "Sizin mahremiyetiniz ve reklamlar Modrinth'i nasıl destekler"
},
- "app.appearance-settings.advanced-rendering.description": {
- "message": "Arka plan bulanıklığı gibi görsel efektleri etkinleştirin. Bu, donanım ivmelendirme yokken performansı etkileyebilir."
- },
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Gelişmiş işleme"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "Modrinth App tarafından kullanılacak renk şemasını seçin."
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Renk teması"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Ana Sayfa"
},
@@ -179,12 +167,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Uygulamayı küçült"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "İşletim sisteminizin başlık çubuğu ve pencere kontrollerini kullanın. Uygulamanın yeniden başlatılmasını gerektirir."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Sistem pencere çerçevesi"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Her bir örneği ne kadar süreyle oynadığını göster."
},
diff --git a/apps/app-frontend/src/locales/uk-UA/index.json b/apps/app-frontend/src/locales/uk-UA/index.json
index f2f02758b7..5e16a46475 100644
--- a/apps/app-frontend/src/locales/uk-UA/index.json
+++ b/apps/app-frontend/src/locales/uk-UA/index.json
@@ -143,18 +143,6 @@
"app.ads-consent.title": {
"message": "Ваша приватність та як реклама підтримує Modrinth"
},
- "app.appearance-settings.advanced-rendering.description": {
- "message": "Умикає візуальні ефекти, як-от розмиття фону. Це може знизити продуктивність без апаратного прискорення."
- },
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Розширене промальовування"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "Оберіть тему для Modrinth App."
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Колірна тема"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Головна"
},
@@ -179,12 +167,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Згортати застосунок"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Використовувати заголовок вікна та елементи керування вашої операційної системи. Потребує перезапуску застосунку."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Системна рамка вікна"
- },
"app.appearance-settings.show-play-time.description": {
"message": "Показувати награний час для кожної збірки."
},
diff --git a/apps/app-frontend/src/locales/vi-VN/index.json b/apps/app-frontend/src/locales/vi-VN/index.json
index 488a8dbaa5..9114a99ea7 100644
--- a/apps/app-frontend/src/locales/vi-VN/index.json
+++ b/apps/app-frontend/src/locales/vi-VN/index.json
@@ -134,12 +134,6 @@
"app.ads-consent.title": {
"message": "Quyền riêng tư của bạn và cách quảng cáo hỗ trợ Modrinth"
},
- "app.appearance-settings.advanced-rendering.title": {
- "message": "Dựng hình nâng cao"
- },
- "app.appearance-settings.color-theme.title": {
- "message": "Màu chủ đề"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "Trang chủ"
},
@@ -161,12 +155,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Thu nhỏ launcher"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "Sử dụng khung cửa sổ hệ thống (yêu cầu khởi động lại ứng dụng)."
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "Giao diện cửa sổ hệ thống"
- },
"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."
},
diff --git a/apps/app-frontend/src/locales/zh-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json
index ac83dc137a..4d12f1cbee 100644
--- a/apps/app-frontend/src/locales/zh-CN/index.json
+++ b/apps/app-frontend/src/locales/zh-CN/index.json
@@ -143,18 +143,6 @@
"app.ads-consent.title": {
"message": "您的隐私以及广告如何支持 Modrinth"
},
- "app.appearance-settings.advanced-rendering.description": {
- "message": "启用模糊效果等高级渲染。可能会在没有硬件加速渲染的情况下造成性能问题。"
- },
- "app.appearance-settings.advanced-rendering.title": {
- "message": "高级渲染"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "为 Modrinth App 选择偏好的色彩主题。"
- },
- "app.appearance-settings.color-theme.title": {
- "message": "色彩主题"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "首页"
},
@@ -179,12 +167,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "最小化 App"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "使用操作系统的标题栏和窗口控件。需要重启应用。"
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "系统窗口框架"
- },
"app.appearance-settings.show-play-time.description": {
"message": "显示你在每个实例中游玩的时间。"
},
diff --git a/apps/app-frontend/src/locales/zh-TW/index.json b/apps/app-frontend/src/locales/zh-TW/index.json
index bd4d82f1ca..d970cb8444 100644
--- a/apps/app-frontend/src/locales/zh-TW/index.json
+++ b/apps/app-frontend/src/locales/zh-TW/index.json
@@ -143,18 +143,6 @@
"app.ads-consent.title": {
"message": "關於你的隱私,以及廣告如何幫助 Modrinth"
},
- "app.appearance-settings.advanced-rendering.description": {
- "message": "啟用視覺效果(如背景模糊),若無硬體加速可能會降低效能。"
- },
- "app.appearance-settings.advanced-rendering.title": {
- "message": "進階繪製"
- },
- "app.appearance-settings.color-theme.description": {
- "message": "選擇 Modrinth App 使用的色彩主題。"
- },
- "app.appearance-settings.color-theme.title": {
- "message": "色彩主題"
- },
"app.appearance-settings.default-landing-page.home": {
"message": "首頁"
},
@@ -179,12 +167,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "最小化應用程式"
},
- "app.appearance-settings.native-decorations.description": {
- "message": "使用作業系統的標題列和視窗控制項。需要重新啟動應用程式。"
- },
- "app.appearance-settings.native-decorations.title": {
- "message": "系統視窗外框"
- },
"app.appearance-settings.show-play-time.description": {
"message": "顯示你在各個實例的遊玩時數。"
},
diff --git a/apps/app-frontend/src/main.js b/apps/app-frontend/src/main.js
index c9dfd2b342..99f4c88ca4 100644
--- a/apps/app-frontend/src/main.js
+++ b/apps/app-frontend/src/main.js
@@ -5,7 +5,6 @@ import * as Sentry from '@sentry/vue'
import { VueScanPlugin } from '@taijased/vue-render-tracker'
import { VueQueryPlugin } from '@tanstack/vue-query'
import FloatingVue from 'floating-vue'
-import { createPinia } from 'pinia'
import { createApp } from 'vue'
import App from '@/App.vue'
@@ -21,8 +20,6 @@ const vueScan = new VueScanPlugin({
playSound: false, // Play sound on each render
})
-const pinia = createPinia()
-
let app = createApp(App)
Sentry.init({
@@ -35,7 +32,6 @@ Sentry.init({
app.use(VueQueryPlugin)
app.use(vueScan)
app.use(router)
-app.use(pinia)
app.use(FloatingVue, {
themes: {
'ribbit-popout': {
diff --git a/apps/app-frontend/src/pages/Browse.vue b/apps/app-frontend/src/pages/Browse.vue
index 0a7a18263d..83fab798f1 100644
--- a/apps/app-frontend/src/pages/Browse.vue
+++ b/apps/app-frontend/src/pages/Browse.vue
@@ -41,6 +41,7 @@ import { useRoute, useRouter } from 'vue-router'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import { useAppServerBrowse } from '@/composables/browse/use-app-server-browse'
import { useAppEvent } from '@/composables/use-app-event'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
import { get_project, get_search_results_v3, get_version_many } from '@/helpers/cache.js'
import {
get_installed_project_ids as getInstalledProjectIds,
@@ -63,7 +64,6 @@ import {
createServerInstallContent,
provideServerInstallContent,
} from '@/providers/setup/server-install-content'
-import { useTheming } from '@/store/state'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
@@ -109,7 +109,7 @@ const breadcrumbLabel = computed(() => {
),
})
})
-const themeStore = useTheming()
+const appSettings = useAppSettings()
const browseRouteActive = computed(() => route.path.startsWith('/browse/'))
const serverSetupModalRef = ref | null>(null)
const serverInstallContent = createServerInstallContent({ serverSetupModalRef })
@@ -384,7 +384,7 @@ async function initInstanceContext() {
}
function setBrowseHideInstalledFlag(flag: 'hide_installed_modpacks', value: boolean) {
- themeStore.featureFlags[flag] = value
+ appSettings.featureFlags[flag] = value
getSettings()
.then((settings) => {
settings.feature_flags[flag] = value
@@ -394,7 +394,7 @@ function setBrowseHideInstalledFlag(flag: 'hide_installed_modpacks', value: bool
}
const hideInstalledModpacks = computed({
- get: () => themeStore.getFeatureFlag('hide_installed_modpacks'),
+ get: () => appSettings.getFeatureFlag('hide_installed_modpacks'),
set: (value: boolean) => setBrowseHideInstalledFlag('hide_installed_modpacks', value),
})
@@ -742,7 +742,7 @@ const installContext = computed(() => {
queuedCount: queuedServerInstallCount.value,
selectedProjects: selectedServerInstallProjects.value,
isInstallingSelected: isInstallingQueuedServerInstalls.value,
- skipNonEssentialWarnings: themeStore.getFeatureFlag('skip_non_essential_warnings'),
+ skipNonEssentialWarnings: appSettings.getFeatureFlag('skip_non_essential_warnings'),
installProgress: queuedInstallProgress.value,
clearQueued: clearQueuedServerInstalls,
clearSelected: clearQueuedServerInstalls,
@@ -1222,9 +1222,9 @@ function getProjectBrowseQuery() {
}
const advancedFiltersCollapsed = computed({
- get: () => themeStore.getFeatureFlag('advanced_filters_collapsed'),
+ get: () => appSettings.getFeatureFlag('advanced_filters_collapsed'),
set: (value) => {
- themeStore.featureFlags['advanced_filters_collapsed'] = value
+ appSettings.featureFlags['advanced_filters_collapsed'] = value
getSettings()
.then((settings) => {
settings.feature_flags['advanced_filters_collapsed'] = value
@@ -1235,9 +1235,9 @@ const advancedFiltersCollapsed = computed({
})
const dismissedPhotosensitivityFilterWarning = computed({
- get: () => themeStore.getFeatureFlag('dismissed_photosensitivity_filter_warning'),
+ get: () => appSettings.getFeatureFlag('dismissed_photosensitivity_filter_warning'),
set: (value) => {
- themeStore.featureFlags['dismissed_photosensitivity_filter_warning'] = value
+ appSettings.featureFlags['dismissed_photosensitivity_filter_warning'] = value
getSettings()
.then((settings) => {
settings.feature_flags['dismissed_photosensitivity_filter_warning'] = value
diff --git a/apps/app-frontend/src/pages/Index.vue b/apps/app-frontend/src/pages/Index.vue
index 9e7c51fd1c..04a9b84833 100644
--- a/apps/app-frontend/src/pages/Index.vue
+++ b/apps/app-frontend/src/pages/Index.vue
@@ -9,12 +9,12 @@ import LibrarySection from '@/components/ui/library/index.vue'
import WelcomeScreen from '@/components/ui/WelcomeScreen.vue'
import RecentWorldsList from '@/components/ui/world/RecentWorldsList.vue'
import { useAppEvent } from '@/composables/use-app-event'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
import { toError } from '@/helpers/errors'
import { list } from '@/helpers/instance'
import type { GameInstance } from '@/helpers/types'
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
import { injectOnboardingChecklist } from '@/providers/onboarding-checklist'
-import { useTheming } from '@/store/theme.ts'
defineOptions({
name: 'LibraryPage',
@@ -25,7 +25,7 @@ const { formatMessage } = useVIntl()
const { hasCreatedInstance, isReady } = injectOnboardingChecklist()
const showCreationModal = inject<() => void>('showCreationModal')
const pageOptions = ref>()
-const themeStore = useTheming()
+const appSettings = useAppSettings()
const messages = defineMessages({
home: {
@@ -106,7 +106,7 @@ function handlePageOption({ option }: { option: string }) {
@contextmenu="openPageContextMenu"
>
diff --git a/apps/app-frontend/src/pages/Skins.vue b/apps/app-frontend/src/pages/Skins.vue
index 40a4597fea..a265eed553 100644
--- a/apps/app-frontend/src/pages/Skins.vue
+++ b/apps/app-frontend/src/pages/Skins.vue
@@ -32,6 +32,8 @@ import EarsModIcon from '@/assets/skins/ears-mod.png'
import type AccountsCard from '@/components/ui/AccountsCard.vue'
import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue'
import VirtualSkinSectionList from '@/components/ui/skin/VirtualSkinSectionList.vue'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
+import { handleSevereError } from '@/composables/use-error.js'
import { trackEvent } from '@/helpers/analytics'
import { check_reachable, get_default_user, login as login_flow, users } from '@/helpers/auth'
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
@@ -58,8 +60,6 @@ import {
} from '@/helpers/skins.ts'
import { hasPride26Badge } from '@/helpers/user-campaigns.ts'
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
-import { handleSevereError } from '@/store/error'
-import { useTheming } from '@/store/state'
import { appMessages } from '@/utils/app-messages'
useRootBreadcrumb({
@@ -217,7 +217,7 @@ const { addNotification, handleError } = notifications
const auth = injectAuth()
const client = injectModrinthClient()
-const themeStore = useTheming()
+const appSettings = useAppSettings()
const skins = ref([])
const capes = ref([])
const offline = ref(!navigator.onLine)
@@ -339,7 +339,7 @@ const skinTexture = computedAsync(async () => {
})
const capeTexture = computed(() => currentCape.value?.texture)
const skinVariant = computed(() => selectedSkin.value?.variant)
-const skinNametag = computed(() => (themeStore.hideNametagSkinsPage ? undefined : username.value))
+const skinNametag = computed(() => (appSettings.hideNametagSkinsPage ? undefined : username.value))
const isSkinManagementReadOnly = computed(
() =>
!!currentUser.value &&
diff --git a/apps/app-frontend/src/pages/hosting/manage/Index.vue b/apps/app-frontend/src/pages/hosting/manage/Index.vue
index a90ee87076..e182d1386f 100644
--- a/apps/app-frontend/src/pages/hosting/manage/Index.vue
+++ b/apps/app-frontend/src/pages/hosting/manage/Index.vue
@@ -8,7 +8,7 @@
:layout-mode="isContainedServerRoute ? 'contained' : 'page'"
:reload-page="() => router.go(0)"
:resolve-viewer="resolveViewer"
- :show-copy-id-action="themeStore.devMode"
+ :show-copy-id-action="appSettings.devMode"
:auth-user="authUser"
:navigate-to-billing="() => openUrl('https://modrinth.com/settings/billing')"
:navigate-to-servers="() => router.push('/hosting/manage')"
@@ -61,17 +61,17 @@ import { openUrl } from '@tauri-apps/plugin-opener'
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
import { get_user } from '@/helpers/cache'
import { get as getCreds } from '@/helpers/mr_auth'
import { provideBreadcrumbParent, useBreadcrumb } from '@/providers/breadcrumbs'
-import { useTheming } from '@/store/theme'
const route = useRoute()
const router = useRouter()
const auth = injectAuth()
const client = injectModrinthClient()
const queryClient = useQueryClient()
-const themeStore = useTheming()
+const appSettings = useAppSettings()
const { formatMessage } = useVIntl()
const isContainedServerRoute = computed(() => route.name === 'ServerManageOverview')
diff --git a/apps/app-frontend/src/pages/instance/components/settings-modal/installation-settings.vue b/apps/app-frontend/src/pages/instance/components/settings-modal/installation-settings.vue
index f2a6e9206e..2dc6d6b97f 100644
--- a/apps/app-frontend/src/pages/instance/components/settings-modal/installation-settings.vue
+++ b/apps/app-frontend/src/pages/instance/components/settings-modal/installation-settings.vue
@@ -16,6 +16,7 @@ import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
import { useManagedContentPolicy } from '@/composables/instances/use-managed-content-policy'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
import { trackEvent } from '@/helpers/analytics'
import { get_project_versions, get_version } from '@/helpers/cache'
import {
@@ -34,7 +35,6 @@ import { get_loader_versions } from '@/helpers/metadata'
import { get_game_versions, get_loaders } from '@/helpers/tags'
import { injectAppEvents } from '@/providers/app-events'
import { provideInstanceBackup } from '@/providers/instance-backup'
-import { useTheming } from '@/store/state'
import type { Manifest } from '../../../../helpers/types'
import { instanceKeys } from '../../query-options.ts'
@@ -47,12 +47,12 @@ const filePicker = injectFilePicker()
const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
const debug = useDebugLogger('AppInstallationSettings')
-const themeStore = useTheming()
+const appSettings = useAppSettings()
const { instance, offline, isMinecraftServer, onUnlinked, closeModal } = injectInstanceSettings()
const managedContentPolicy = useManagedContentPolicy(instance)
const skipNonEssentialWarnings = computed(() =>
- themeStore.getFeatureFlag('skip_non_essential_warnings'),
+ appSettings.getFeatureFlag('skip_non_essential_warnings'),
)
debug('metadata load: start', {
diff --git a/apps/app-frontend/src/pages/instance/content/index.vue b/apps/app-frontend/src/pages/instance/content/index.vue
index fe22753605..490ae5ca0f 100644
--- a/apps/app-frontend/src/pages/instance/content/index.vue
+++ b/apps/app-frontend/src/pages/instance/content/index.vue
@@ -127,6 +127,7 @@ import ExportModal from '@/components/ui/ExportModal.vue'
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
import { useManagedContentPolicy } from '@/composables/instances/use-managed-content-policy'
import { useAppEvent } from '@/composables/use-app-event'
+import { type FeatureFlag, useAppSettings } from '@/composables/use-app-settings.ts'
import { trackEvent } from '@/helpers/analytics'
import { get_project_versions, get_version, get_version_many } from '@/helpers/cache.js'
import {
@@ -149,8 +150,6 @@ import type { CacheBehaviour } from '@/helpers/types'
import { highlightModInInstance } from '@/helpers/utils.js'
import { type AppEventPayload, injectAppEvents } from '@/providers/app-events'
import { injectContentInstall } from '@/providers/content-install'
-import { useTheming } from '@/store/state'
-import type { FeatureFlag } from '@/store/theme'
import { injectInstancePage } from '../instance-context'
import { instanceContentQueryOptions, instanceKeys } from '../query-options'
@@ -234,10 +233,10 @@ const { installingItems, installRevisionByInstance, installFailureRevisionByInst
const router = useRouter()
const queryClient = useQueryClient()
const debug = useDebugLogger('Mods:ContentUpdate')
-const themeStore = useTheming()
+const appSettings = useAppSettings()
const skipUnknownFileWarningFeatureFlag = 'skip_unknown_pack_warning' as FeatureFlag
const skipNonEssentialWarnings = computed(() =>
- themeStore.getFeatureFlag('skip_non_essential_warnings'),
+ appSettings.getFeatureFlag('skip_non_essential_warnings'),
)
const instancePage = injectInstancePage()
@@ -759,7 +758,7 @@ async function handleUploadFiles() {
}
function confirmUnknownFileInstallation(fileName: string) {
- if (themeStore.getFeatureFlag(skipUnknownFileWarningFeatureFlag)) {
+ if (appSettings.getFeatureFlag(skipUnknownFileWarningFeatureFlag)) {
return Promise.resolve(true)
}
@@ -779,7 +778,7 @@ function resolveUnknownFileWarning(confirmed: boolean) {
async function handleUnknownFileContinue(dontShowAgain: boolean) {
if (dontShowAgain) {
- themeStore.featureFlags[skipUnknownFileWarningFeatureFlag] = true
+ appSettings.featureFlags[skipUnknownFileWarningFeatureFlag] = true
try {
const settings = await getSettings()
settings.feature_flags[skipUnknownFileWarningFeatureFlag] = true
diff --git a/apps/app-frontend/src/pages/instance/layout.vue b/apps/app-frontend/src/pages/instance/layout.vue
index 6a9f27d4f1..ad9655fe45 100644
--- a/apps/app-frontend/src/pages/instance/layout.vue
+++ b/apps/app-frontend/src/pages/instance/layout.vue
@@ -133,6 +133,8 @@ import {
getFreshCachedServerStatus,
} from '@/composables/instances/use-server-status-query'
import { useAppEvent } from '@/composables/use-app-event'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
+import { handleSevereError } from '@/composables/use-error.js'
import { useInstanceConsole } from '@/composables/useInstanceConsole'
import { trackEvent } from '@/helpers/analytics'
import { toError } from '@/helpers/errors'
@@ -159,8 +161,6 @@ import type { ServerStatus } from '@/helpers/worlds'
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
import { provideInstanceBackup } from '@/providers/instance-backup'
import { injectServerInstall } from '@/providers/server-install'
-import { handleSevereError } from '@/store/error.js'
-import { useTheming } from '@/store/state'
import InstanceAdmonitions from './components/admonitions/index.vue'
import InstancePageHeader from './components/page-header/index.vue'
@@ -216,8 +216,8 @@ watch(
},
{ immediate: true },
)
-const themeStore = useTheming()
-const showInstancePlayTime = computed(() => themeStore.getFeatureFlag('show_instance_play_time'))
+const appSettings = useAppSettings()
+const showInstancePlayTime = computed(() => appSettings.getFeatureFlag('show_instance_play_time'))
const online = useOnline()
const offline = computed(() => !online.value)
diff --git a/apps/app-frontend/src/pages/instance/worlds/index.vue b/apps/app-frontend/src/pages/instance/worlds/index.vue
index 3539b608ca..b4b9583f73 100644
--- a/apps/app-frontend/src/pages/instance/worlds/index.vue
+++ b/apps/app-frontend/src/pages/instance/worlds/index.vue
@@ -157,6 +157,7 @@ import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
import EditWorldModal from '@/components/ui/world/modal/EditSingleplayerWorldModal.vue'
import WorldItem from '@/components/ui/world/WorldItem.vue'
import { useAppEvent } from '@/composables/use-app-event'
+import { handleSevereError } from '@/composables/use-error.js'
import { trackEvent } from '@/helpers/analytics'
import { get_project, get_project_v3 } from '@/helpers/cache.js'
import { get_game_versions } from '@/helpers/tags'
@@ -186,7 +187,6 @@ import {
type World,
} from '@/helpers/worlds.ts'
import { injectServerInstall } from '@/providers/server-install'
-import { handleSevereError } from '@/store/error.js'
import { injectInstancePage } from '../instance-context'
import { instanceKeys, instanceWorldsQueryOptions } from '../query-options'
diff --git a/apps/app-frontend/src/pages/project/Index.vue b/apps/app-frontend/src/pages/project/Index.vue
index 798a9e97e6..13f5416b6d 100644
--- a/apps/app-frontend/src/pages/project/Index.vue
+++ b/apps/app-frontend/src/pages/project/Index.vue
@@ -55,7 +55,7 @@
@@ -294,6 +294,7 @@ import {
getFreshCachedServerStatus,
} from '@/composables/instances/use-server-status-query'
import { useAppEvent } from '@/composables/use-app-event'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
import {
get_organization,
get_project,
@@ -317,7 +318,6 @@ import { provideBreadcrumbParent, useBreadcrumb } from '@/providers/breadcrumbs'
import { injectContentInstall } from '@/providers/content-install'
import { injectServerInstall } from '@/providers/server-install'
import { createServerInstallContent } from '@/providers/setup/server-install-content'
-import { useTheming } from '@/store/state.js'
dayjs.extend(relativeTime)
@@ -348,7 +348,7 @@ const projectBreadcrumbTo = computed(() => {
return currentRoute.fullPath
})
const queryClient = useQueryClient()
-const themeStore = useTheming()
+const appSettings = useAppSettings()
const { formatMessage } = useVIntl()
const messages = defineMessages({
diff --git a/apps/app-frontend/src/pages/project/Versions.vue b/apps/app-frontend/src/pages/project/Versions.vue
index d723a634b8..2893452391 100644
--- a/apps/app-frontend/src/pages/project/Versions.vue
+++ b/apps/app-frontend/src/pages/project/Versions.vue
@@ -5,7 +5,7 @@
:game-versions="gameVersions"
:versions="versions"
:project="project"
- :show-environment-column="themeStore.featureFlags.show_version_environment_column"
+ :show-environment-column="appSettings.featureFlags.show_version_environment_column"
:version-link="(version) => buildProjectHref(`/project/${project.id}/version/${version.id}`)"
>
@@ -63,11 +63,11 @@ import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { SwapIcon } from '@/assets/icons/index.js'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
import { get_game_versions, get_loaders } from '@/helpers/tags.js'
-import { useTheming } from '@/store/theme.ts'
const { formatMessage } = useVIntl()
-const themeStore = useTheming()
+const appSettings = useAppSettings()
const messages = defineMessages({
alreadyInstalled: {
diff --git a/apps/app-frontend/src/plugins/i18n-debug.ts b/apps/app-frontend/src/plugins/i18n-debug.ts
index 76fac0851c..9a32684cb3 100644
--- a/apps/app-frontend/src/plugins/i18n-debug.ts
+++ b/apps/app-frontend/src/plugins/i18n-debug.ts
@@ -9,13 +9,13 @@ import {
import type { App } from 'vue'
import { reactive, ref, watch } from 'vue'
-import { useTheming } from '@/store/theme'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
export default {
install(app: App) {
- const theming = useTheming()
+ const appSettings = useAppSettings()
- const enabled = ref(theming.featureFlags.i18n_debug ?? false)
+ const enabled = ref(appSettings.featureFlags.i18n_debug ?? false)
const keyReveal = ref(false)
const registry = reactive(new Map()) as Map<
string,
@@ -29,7 +29,7 @@ export default {
initI18nDebugRuntime(context)
watch(
- () => theming.featureFlags.i18n_debug,
+ () => appSettings.featureFlags.i18n_debug,
(active) => {
enabled.value = active
if (!active) {
diff --git a/apps/app-frontend/src/providers/content-install.ts b/apps/app-frontend/src/providers/content-install.ts
index f1ca9bbc69..72f34a1561 100644
--- a/apps/app-frontend/src/providers/content-install.ts
+++ b/apps/app-frontend/src/providers/content-install.ts
@@ -11,6 +11,7 @@ import dayjs from 'dayjs'
import { nextTick, type Ref, ref } from 'vue'
import type { Router } from 'vue-router'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
import { trackEvent } from '@/helpers/analytics'
import {
get_organization,
@@ -38,7 +39,6 @@ import {
import { get_game_versions } from '@/helpers/tags'
import type { GameInstance, InstanceLoader } from '@/helpers/types'
import type { AppEvents } from '@/providers/app-events'
-import { useTheming } from '@/store/state'
interface ModalRef {
show: (initialVersionId?: string) => void
hide: () => void
@@ -188,7 +188,7 @@ export function createContentInstall(opts: {
appEvents: AppEvents
}): ContentInstallContext {
const { formatMessage } = useVIntl()
- const themeStore = useTheming()
+ const appSettings = useAppSettings()
const instances = ref([])
const compatibleLoaders = ref([])
const gameVersions = ref([])
@@ -858,7 +858,7 @@ export function createContentInstall(opts: {
const packs = await list()
const existingPack = packs.find((pack) => pack.link?.project_id === project.id)
- if (existingPack && !themeStore.getFeatureFlag('skip_non_essential_warnings')) {
+ if (existingPack && !appSettings.getFeatureFlag('skip_non_essential_warnings')) {
pendingModpackInstall = { project, version, source, callback, createInstanceCallback }
modpackAlreadyInstalledModalRef?.show(existingPack.name, existingPack.id)
return
diff --git a/apps/app-frontend/src/providers/server-install.ts b/apps/app-frontend/src/providers/server-install.ts
index 8cd201919f..ec8f2a56b6 100644
--- a/apps/app-frontend/src/providers/server-install.ts
+++ b/apps/app-frontend/src/providers/server-install.ts
@@ -4,6 +4,7 @@ import { createContext } from '@modrinth/ui'
import { type Ref, ref } from 'vue'
import type { Router } from 'vue-router'
+import { handleSevereError } from '@/composables/use-error.js'
import { trackEvent } from '@/helpers/analytics'
import { get_project, get_project_v3, get_version } from '@/helpers/cache.js'
import {
@@ -18,7 +19,6 @@ import type { GameInstance } from '@/helpers/types'
import { ensureManagedServerWorldExists, getServerAddress } from '@/helpers/worlds'
import { start_join_server } from '@/helpers/worlds.ts'
import type { AppEvents } from '@/providers/app-events'
-import { handleSevereError } from '@/store/error.js'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface ModalRef void = () => void> {
diff --git a/apps/app-frontend/src/providers/setup/auth.ts b/apps/app-frontend/src/providers/setup/auth.ts
index 57bde7d324..372364ad0d 100644
--- a/apps/app-frontend/src/providers/setup/auth.ts
+++ b/apps/app-frontend/src/providers/setup/auth.ts
@@ -46,4 +46,5 @@ export function setupAuthProvider(
})
provideAuth(authProvider)
+ return authProvider
}
diff --git a/apps/app-frontend/src/providers/setup/creation-modal.ts b/apps/app-frontend/src/providers/setup/creation-modal.ts
index 1ada296ba4..4ede41c8d3 100644
--- a/apps/app-frontend/src/providers/setup/creation-modal.ts
+++ b/apps/app-frontend/src/providers/setup/creation-modal.ts
@@ -9,6 +9,7 @@ import { useRouter } from 'vue-router'
import type UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue'
import type ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue'
+import { useAppSettings } from '@/composables/use-app-settings.ts'
import { trackEvent } from '@/helpers/analytics'
import { get_search_results } from '@/helpers/cache.js'
import { import_instance } from '@/helpers/import.js'
@@ -22,7 +23,6 @@ import {
import { list } from '@/helpers/instance'
import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata.js'
import type { InstanceIconConfig, InstanceLoader } from '@/helpers/types'
-import { useTheming } from '@/store/state'
export function setupCreationModal(
notificationManager: AbstractWebNotificationManager,
@@ -30,7 +30,7 @@ export function setupCreationModal(
) {
const { handleError } = notificationManager
const router = useRouter()
- const themeStore = useTheming()
+ const appSettings = useAppSettings()
const installationModal =
useTemplateRef>('installationModal')
@@ -97,7 +97,7 @@ export function setupCreationModal(
const instances = await list().catch(handleError)
const existingInstance = instances?.find((i) => i.link?.project_id === projectId)
- if (existingInstance && !themeStore.getFeatureFlag('skip_non_essential_warnings')) {
+ if (existingInstance && !appSettings.getFeatureFlag('skip_non_essential_warnings')) {
pendingModpackCreation.value = { projectId, versionId, name, iconUrl }
installationModal.value?.hide()
modpackAlreadyInstalledModal.value?.show(existingInstance.name, existingInstance.id)
diff --git a/apps/app-frontend/src/providers/setup/loading-state.ts b/apps/app-frontend/src/providers/setup/loading-state.ts
index 21d3ed1fe9..36a9e1e843 100644
--- a/apps/app-frontend/src/providers/setup/loading-state.ts
+++ b/apps/app-frontend/src/providers/setup/loading-state.ts
@@ -4,7 +4,7 @@ import { createLoadingStateCore, provideLoadingState } from '@modrinth/ui'
/**
* Source of truth for the desktop app's loading state.
*
- * Owns the token-based ref-counter directly (no Pinia store). Consumers
+ * Owns the token-based ref-counter directly. Consumers
* obtain the same reactive state via `injectLoadingState()` from `@modrinth/ui`.
*
* Returns the provider so the call site (App.vue) can also use it directly
diff --git a/apps/app-frontend/src/providers/setup/user-preferences.ts b/apps/app-frontend/src/providers/setup/user-preferences.ts
new file mode 100644
index 0000000000..d7bc88c15f
--- /dev/null
+++ b/apps/app-frontend/src/providers/setup/user-preferences.ts
@@ -0,0 +1,19 @@
+import {
+ type AbstractWebNotificationManager,
+ type AuthProvider,
+ setupUserPreferencesProvider,
+} from '@modrinth/ui'
+
+import { get_user_preferences, patch_user_preferences } from '@/helpers/user-preferences.ts'
+
+export function setupAppUserPreferencesProvider(
+ auth: AuthProvider,
+ notificationManager: AbstractWebNotificationManager,
+) {
+ return setupUserPreferencesProvider({
+ auth,
+ getPreferences: get_user_preferences,
+ patchPreferences: patch_user_preferences,
+ notificationManager,
+ })
+}
diff --git a/apps/app-frontend/src/store/error.js b/apps/app-frontend/src/store/error.js
deleted file mode 100644
index 56953f263e..0000000000
--- a/apps/app-frontend/src/store/error.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { defineStore } from 'pinia'
-
-import { findMinecraftAuthError } from '@/components/ui/minecraft-auth-error-modal/minecraft-auth-errors'
-
-export const useError = defineStore('errorsStore', {
- state: () => ({
- errorModal: null,
- minecraftAuthErrorModal: null,
- minecraftRequiredModal: null,
- }),
- actions: {
- setErrorModal(ref) {
- this.errorModal = ref
- },
- setMinecraftAuthErrorModal(ref) {
- this.minecraftAuthErrorModal = ref
- },
- setMinecraftRequiredModal(ref) {
- this.minecraftRequiredModal = ref
- },
- showError(error, context, closable = true, source = null) {
- const errorMessage = error.message?.toLowerCase()
- if (
- (errorMessage?.includes('user is not logged in') ||
- errorMessage?.includes('cannot play instance since minecraft is required')) &&
- this.minecraftRequiredModal
- ) {
- this.minecraftRequiredModal.show()
- return
- }
- if (
- error.message &&
- (error.message.includes('Minecraft authentication error:') ||
- findMinecraftAuthError(error.message)) &&
- this.minecraftAuthErrorModal
- ) {
- this.minecraftAuthErrorModal.show(error)
- return
- }
- this.errorModal.show(error, context, closable, source)
- },
- },
-})
-
-export const handleSevereError = (err, context) => {
- const error = useError()
- error.showError(err, context)
- console.error(err)
-}
diff --git a/apps/app-frontend/src/store/state.js b/apps/app-frontend/src/store/state.js
deleted file mode 100644
index b5fa69692c..0000000000
--- a/apps/app-frontend/src/store/state.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import { useTheming } from './theme.ts'
-
-export { useTheming }
diff --git a/apps/app-frontend/src/store/theme.ts b/apps/app-frontend/src/store/theme.ts
deleted file mode 100644
index 8e0fe8f2b7..0000000000
--- a/apps/app-frontend/src/store/theme.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { defineStore } from 'pinia'
-
-let systemThemeMq: MediaQueryList | null = null
-
-export const DEFAULT_FEATURE_FLAGS = {
- project_background: false,
- page_path: false,
- worlds_in_home: true,
- server_project_qa: false,
- show_version_environment_column: false,
- server_ram_as_bytes_always_on: false,
- always_show_app_controls: false,
- skip_non_essential_warnings: false,
- skip_unknown_pack_warning: false,
- pride_fundraiser: true,
- i18n_debug: false,
- show_instance_play_time: true,
- advanced_filters_collapsed: true,
- always_show_copy_details: false,
- hide_installed_modpacks: false,
- friends_active_collapsed: false,
- friends_online_collapsed: false,
- friends_offline_collapsed: true,
- friends_pending_collapsed: true,
- dismissed_photosensitivity_filter_warning: false,
-}
-
-export const THEME_OPTIONS = ['dark', 'light', 'oled', 'retro', 'system'] as const
-
-export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
-export type FeatureFlags = Record
-export type ColorTheme = (typeof THEME_OPTIONS)[number]
-
-export type ThemeStore = {
- selectedTheme: ColorTheme
- advancedRendering: boolean
- hideNametagSkinsPage: boolean
- toggleSidebar: boolean
-
- devMode: boolean
- featureFlags: FeatureFlags
-}
-
-export const DEFAULT_THEME_STORE: ThemeStore = {
- selectedTheme: 'dark',
- advancedRendering: true,
- hideNametagSkinsPage: false,
- toggleSidebar: false,
-
- devMode: false,
- featureFlags: DEFAULT_FEATURE_FLAGS,
-}
-
-export const useTheming = defineStore('themeStore', {
- state: () => DEFAULT_THEME_STORE,
- actions: {
- setThemeState(newTheme: ColorTheme) {
- if (THEME_OPTIONS.includes(newTheme)) {
- this.selectedTheme = newTheme
- } else {
- console.warn('Selected theme is not present. Check themeOptions.')
- }
-
- this.setThemeClass()
- },
- setThemeClass() {
- const html = document.getElementsByTagName('html')[0]
- for (const theme of THEME_OPTIONS) {
- html.classList.remove(`${theme}-mode`)
- }
-
- systemThemeMq?.removeEventListener('change', this.setThemeClass)
- systemThemeMq = null
-
- let theme = this.selectedTheme
- if (this.selectedTheme === 'system') {
- systemThemeMq = window.matchMedia('(prefers-color-scheme: dark)')
- systemThemeMq.addEventListener('change', this.setThemeClass)
- theme = systemThemeMq.matches ? 'dark' : 'light'
- }
-
- html.classList.add(`${theme}-mode`)
- },
- getFeatureFlag(key: FeatureFlag) {
- return this.featureFlags[key] ?? DEFAULT_FEATURE_FLAGS[key]
- },
- getThemeOptions() {
- return THEME_OPTIONS
- },
- },
-})
diff --git a/apps/app/build.rs b/apps/app/build.rs
index 50798f5ca5..f1347fe3e6 100644
--- a/apps/app/build.rs
+++ b/apps/app/build.rs
@@ -303,7 +303,11 @@ fn main() {
"get_user_projects",
"get_user_organizations",
"get_user_collections",
+ "get_user_preferences",
+ "patch_user_preferences",
"patch_user",
+ "change_user_avatar",
+ "delete_user_avatar",
"block_user",
"unblock_user",
"get_blocked_users",
diff --git a/apps/app/src/api/users.rs b/apps/app/src/api/users.rs
index eae636a7d6..e49089f1c3 100644
--- a/apps/app/src/api/users.rs
+++ b/apps/app/src/api/users.rs
@@ -27,6 +27,19 @@ pub async fn get_user_collections(user_id: &str) -> Result {
Ok(theseus::users::get_user_collections(user_id).await?)
}
+#[tauri::command]
+pub async fn get_user_preferences(user_id: &str) -> Result {
+ Ok(theseus::users::get_user_preferences(user_id).await?)
+}
+
+#[tauri::command]
+pub async fn patch_user_preferences(
+ user_id: &str,
+ preferences: Value,
+) -> Result {
+ Ok(theseus::users::patch_user_preferences(user_id, preferences).await?)
+}
+
#[tauri::command]
pub async fn patch_user(user_id: &str, patch: Value) -> Result<()> {
Ok(theseus::users::patch_user(user_id, patch).await?)
@@ -72,6 +85,8 @@ pub fn init() -> tauri::plugin::TauriPlugin {
get_user_projects,
get_user_organizations,
get_user_collections,
+ get_user_preferences,
+ patch_user_preferences,
patch_user,
change_user_avatar,
delete_user_avatar,
diff --git a/apps/app/tauri.conf.json b/apps/app/tauri.conf.json
index c1b9d08003..9c2f3c031b 100644
--- a/apps/app/tauri.conf.json
+++ b/apps/app/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"build": {
- "beforeDevCommand": "pnpm turbo run dev --filter=@modrinth/app-frontend",
+ "beforeDevCommand": "pnpm -w i18n:coverage && pnpm turbo run dev --filter=@modrinth/app-frontend",
"beforeBuildCommand": "pnpm turbo run build --filter=@modrinth/app-frontend",
"frontendDist": "../app-frontend/dist",
"devUrl": "http://localhost:1420"
diff --git a/apps/frontend/src/app.vue b/apps/frontend/src/app.vue
index 18804d90cf..4208db0280 100644
--- a/apps/frontend/src/app.vue
+++ b/apps/frontend/src/app.vue
@@ -11,13 +11,55 @@
diff --git a/apps/frontend/src/components/ui/NavStack.vue b/apps/frontend/src/components/ui/NavStack.vue
index e71314a55a..ab86be9a04 100644
--- a/apps/frontend/src/components/ui/NavStack.vue
+++ b/apps/frontend/src/components/ui/NavStack.vue
@@ -20,7 +20,7 @@
- {{ item.label }}
+
+ {{ item.label }}
+
{{ String(item.badge) }}
@@ -55,10 +61,16 @@
aria-hidden="true"
class="h-5 w-5 shrink-0"
/>
- {{ item.label }}
+
+ {{ item.label }}
+
{{ String(item.badge) }}
@@ -73,8 +85,8 @@
diff --git a/apps/frontend/src/pages/settings/language.vue b/apps/frontend/src/pages/settings/language.vue
index 612b76f86c..f2c353f31d 100644
--- a/apps/frontend/src/pages/settings/language.vue
+++ b/apps/frontend/src/pages/settings/language.vue
@@ -1,80 +1,31 @@
+
+
+
+
+
+
+
-
-
-
-
- {{ formatMessage(commonSettingsMessages.language) }}
-
-
- {{ formatMessage(languageSelectorMessages.languageWarning, { platform }) }}
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/frontend/src/pages/settings/social.vue b/apps/frontend/src/pages/settings/social.vue
index ade20c47ac..256448d006 100644
--- a/apps/frontend/src/pages/settings/social.vue
+++ b/apps/frontend/src/pages/settings/social.vue
@@ -1,10 +1,18 @@
@@ -14,6 +22,7 @@ import {
AccountSocialSettings,
commonSettingsMessages,
injectModrinthClient,
+ UnsavedChangesPopup,
useVIntl,
} from '@modrinth/ui'
@@ -24,6 +33,12 @@ definePageMeta({
const auth = await useAuth()
const client = injectModrinthClient()
const { formatMessage } = useVIntl()
+const socialSettings = ref | null>(null)
+const emptySocialState = {
+ friendPrivacy: 'everyone',
+ sharedInstancesPrivacy: 'everyone',
+ hostingAccessPrivacy: 'everyone',
+}
function getBlockedUsers(): Promise {
return client.labrinth.blocked_users_v3.list()
diff --git a/apps/frontend/src/plugins/cosmetics.ts b/apps/frontend/src/plugins/cosmetics.ts
index 7d821eae7f..fded0aa3aa 100644
--- a/apps/frontend/src/plugins/cosmetics.ts
+++ b/apps/frontend/src/plugins/cosmetics.ts
@@ -9,6 +9,7 @@ export type DisplayLocation =
| 'modpack'
| 'shader'
| 'datapack'
+ | 'server'
| 'user'
| 'collection'
diff --git a/apps/frontend/src/plugins/i18n.ts b/apps/frontend/src/plugins/i18n.ts
index e1f3dc7d91..e6e8fa0b0e 100644
--- a/apps/frontend/src/plugins/i18n.ts
+++ b/apps/frontend/src/plugins/i18n.ts
@@ -178,7 +178,10 @@ export default defineNuxtPlugin({
return msg
}
- async function setLocale(newLocale: string): Promise {
+ async function setLocale(
+ newLocale: string,
+ { persist = true }: { persist?: boolean } = {},
+ ): Promise {
debug('setLocale: called', { newLocale, currentLocale: locale.value })
if (!LOCALES.some((l) => l.code === newLocale)) {
@@ -197,7 +200,9 @@ export default defineNuxtPlugin({
})
locale.value = newLocale
- useCookie('locale', { maxAge: 31536000, path: '/' }).value = newLocale
+ if (persist) {
+ useCookie('locale', { maxAge: 31536000, path: '/' }).value = newLocale
+ }
}
// Detect initial locale (cookie > Accept-Language > default)
diff --git a/apps/frontend/src/plugins/theme/index.ts b/apps/frontend/src/plugins/theme/index.ts
index 3fb8bf00e9..72c52b3b7f 100644
--- a/apps/frontend/src/plugins/theme/index.ts
+++ b/apps/frontend/src/plugins/theme/index.ts
@@ -3,7 +3,7 @@ import { ref } from 'vue'
import { useNativeTheme } from './native-theme.ts'
import { usePreferredThemes } from './preferred-theme.ts'
import { useThemeSettings } from './theme-settings.ts'
-import { isDarkTheme } from './themes.ts'
+import { isDarkTheme, type Theme } from './themes.ts'
export * from './themes.ts'
@@ -33,8 +33,13 @@ export default defineNuxtPlugin({
}
const $settings = useThemeSettings(() => getPreferredNativeTheme())
+ const $preview = ref(null)
+ const $active = computed(() => {
+ if ($preview.value === null) return $settings.active
+ return $preview.value === 'system' ? getPreferredNativeTheme() : $preview.value
+ })
- useHead({ htmlAttrs: { class: () => [`${$settings.active}-mode`] } })
+ useHead({ htmlAttrs: { class: () => [`${$active.value}-mode`] } })
function syncTheme() {
$settings.active =
@@ -74,6 +79,8 @@ export default defineNuxtPlugin({
provide: {
theme: reactive({
...toRefs($settings),
+ active: $active,
+ preview: $preview,
/**
* Preferred themes for each mode.
*/
diff --git a/apps/frontend/src/plugins/theme/theme-settings.ts b/apps/frontend/src/plugins/theme/theme-settings.ts
index b8a632288a..a9a5fb9d88 100644
--- a/apps/frontend/src/plugins/theme/theme-settings.ts
+++ b/apps/frontend/src/plugins/theme/theme-settings.ts
@@ -3,6 +3,7 @@ import type { Theme } from './themes.ts'
interface ThemeSettings {
preference: Theme | 'system'
value: Theme
+ syncAcrossDevices?: boolean
}
export function useThemeSettings(getDefaultTheme?: () => Theme) {
@@ -22,6 +23,7 @@ export function useThemeSettings(getDefaultTheme?: () => Theme) {
$settings.value = {
preference: 'system',
value: getDefaultTheme(),
+ syncAcrossDevices: true,
}
}
@@ -35,5 +37,10 @@ export function useThemeSettings(getDefaultTheme?: () => Theme) {
get: () => $settings.value.value ?? getDefaultTheme(),
set: (value) => ($settings.value.value = value),
}),
+
+ syncAcrossDevices: computed({
+ get: () => $settings.value.syncAcrossDevices ?? true,
+ set: (value) => ($settings.value.syncAcrossDevices = value),
+ }),
})
}
diff --git a/apps/frontend/src/providers/setup.ts b/apps/frontend/src/providers/setup.ts
index fe4ba192fd..eb969312dc 100644
--- a/apps/frontend/src/providers/setup.ts
+++ b/apps/frontend/src/providers/setup.ts
@@ -1,4 +1,4 @@
-import { provideNotificationManager } from '@modrinth/ui'
+import { provideNotificationManager, setupUserPreferencesProvider } from '@modrinth/ui'
import { FrontendNotificationManager } from './frontend-notifications'
import { setupAuthProvider } from './setup/auth'
@@ -10,13 +10,23 @@ import { setupTagsProvider } from './setup/tags'
import { setupUserCountryProvider } from './setup/user-country'
export function setupProviders(auth: Awaited>) {
- provideNotificationManager(new FrontendNotificationManager())
+ const notificationManager = new FrontendNotificationManager()
+ provideNotificationManager(notificationManager)
- setupAuthProvider(auth)
- setupModrinthClientProvider(auth)
+ const authProvider = setupAuthProvider(auth)
+ const client = setupModrinthClientProvider(auth)
+ const userPreferences = setupUserPreferencesProvider({
+ auth: authProvider,
+ getPreferences: (userId) => client.labrinth.users_v3.getPreferences(userId),
+ patchPreferences: (userId, preferences) =>
+ client.labrinth.users_v3.patchPreferences(userId, preferences),
+ notificationManager,
+ })
setupTagsProvider()
setupFilePickerProvider()
setupPageContextProvider()
setupLoadingStateProvider()
setupUserCountryProvider()
+
+ return userPreferences
}
diff --git a/apps/frontend/src/providers/setup/auth.ts b/apps/frontend/src/providers/setup/auth.ts
index f31ddf5ba4..6842215de4 100644
--- a/apps/frontend/src/providers/setup/auth.ts
+++ b/apps/frontend/src/providers/setup/auth.ts
@@ -30,4 +30,5 @@ export function setupAuthProvider(auth: Awaited>) {
})
provideAuth(authProvider)
+ return authProvider
}
diff --git a/apps/labrinth/src/models/v3/preferences.rs b/apps/labrinth/src/models/v3/preferences.rs
index 1deedfffc2..8db8e69276 100644
--- a/apps/labrinth/src/models/v3/preferences.rs
+++ b/apps/labrinth/src/models/v3/preferences.rs
@@ -7,6 +7,8 @@ pub struct UserPreferences {
#[component(nested)]
pub appearance: AppearancePreferences,
#[component(nested)]
+ pub behavior: BehaviorPreferences,
+ #[component(nested)]
pub localization: LocalizationPreferences,
#[component(nested)]
pub layouts: LayoutPreferences,
@@ -34,6 +36,31 @@ pub struct AppearancePreferences {
pub theme: Theme,
}
+#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq, Component)]
+pub struct BehaviorPreferences {
+ pub minimize_app: bool,
+ pub hide_right_sidebar: bool,
+ pub show_jump_in: bool,
+ pub show_play_time: bool,
+ pub hide_nametag: bool,
+ pub warn_on_unknown_modpacks: bool,
+ pub skip_non_essential_warnings: bool,
+}
+
+impl Default for BehaviorPreferences {
+ fn default() -> Self {
+ Self {
+ minimize_app: false,
+ hide_right_sidebar: false,
+ show_jump_in: true,
+ show_play_time: true,
+ hide_nametag: false,
+ warn_on_unknown_modpacks: true,
+ skip_non_essential_warnings: false,
+ }
+ }
+}
+
#[derive(
Debug, Serialize, Deserialize, ToSchema, Default, Clone, PartialEq,
)]
diff --git a/package.json b/package.json
index edebadb426..4e84fe7f1d 100644
--- a/package.json
+++ b/package.json
@@ -3,12 +3,12 @@
"version": "0.0.0",
"private": true,
"scripts": {
- "web:dev": "turbo run dev --filter=@modrinth/frontend",
+ "web:dev": "pnpm i18n:coverage && turbo run dev --filter=@modrinth/frontend",
"web:build": "turbo run build --filter=@modrinth/frontend",
"app:dev": "turbo run dev --filter=@modrinth/app",
"app:build": "turbo run build --filter=@modrinth/app",
"docs:dev": "turbo run dev --filter=@modrinth/docs",
- "pages:build": "NITRO_PRESET=cloudflare-pages pnpm --filter frontend run build",
+ "pages:build": "pnpm i18n:coverage && NITRO_PRESET=cloudflare-pages pnpm --filter frontend run build",
"build": "turbo run build --continue",
"lint": "turbo run lint lint:ancillary --continue",
"lint:ancillary": "prettier --check .github *.*",
@@ -21,9 +21,10 @@
"prepr:frontend:lib": "turbo run prepr --filter=@modrinth/ui --filter=@modrinth/moderation --filter=@modrinth/assets --filter=@modrinth/blog --filter=@modrinth/api-client --filter=@modrinth/utils --filter=@modrinth/tooling-config",
"prepr:frontend:web": "turbo run prepr --filter=@modrinth/frontend",
"prepr:frontend:app": "turbo run prepr --filter=@modrinth/app-frontend",
- "storybook": "pnpm --filter @modrinth/ui storybook",
- "build-storybook": "pnpm --filter @modrinth/ui build-storybook",
+ "storybook": "pnpm i18n:coverage && pnpm --filter @modrinth/ui storybook",
+ "build-storybook": "pnpm i18n:coverage && pnpm --filter @modrinth/ui build-storybook",
"icons:add": "pnpm --filter @modrinth/assets icons:add",
+ "i18n:coverage": "node scripts/run.mjs coverage-i18n --write packages/ui/src/layouts/wrapped/settings/language-settings/language-settings-coverage.generated.ts --quiet",
"changelog:collect": "node scripts/run.mjs collect-changelog",
"changelog:combine-for-app": "node scripts/run.mjs build-theseus-release-notes",
"scripts": "node scripts/run.mjs"
diff --git a/packages/api-client/src/modules/labrinth/types.ts b/packages/api-client/src/modules/labrinth/types.ts
index d127526687..285e44f4f4 100644
--- a/packages/api-client/src/modules/labrinth/types.ts
+++ b/packages/api-client/src/modules/labrinth/types.ts
@@ -1710,6 +1710,69 @@ export namespace Labrinth {
export type Role = Common.Role
export type AuthProvider = Common.AuthProvider
export type UserPayoutData = Common.UserPayoutData
+ export type Theme = 'light' | 'dark' | 'oled' | 'retro'
+ export type LayoutOption = 'grid' | 'rows'
+ export type FriendPrivacy = 'none' | 'mutual' | 'everyone'
+ export type InvitePrivacy = 'none' | 'friends' | 'everyone'
+
+ export type AppearancePreferences = {
+ auto: boolean
+ theme: Theme
+ }
+
+ export type BehaviorPreferences = {
+ minimize_app: boolean
+ hide_right_sidebar: boolean
+ show_jump_in: boolean
+ show_play_time: boolean
+ hide_nametag: boolean
+ warn_on_unknown_modpacks: boolean
+ skip_non_essential_warnings: boolean
+ }
+
+ export type LocalizationPreferences = {
+ locale: string
+ }
+
+ export type LayoutPreferences = {
+ mods: LayoutOption
+ plugins: LayoutOption
+ datapacks: LayoutOption
+ shaders: LayoutOption
+ resourcepacks: LayoutOption
+ modpacks: LayoutOption
+ servers: LayoutOption
+ users: LayoutOption
+ }
+
+ export type SidebarPreferences = {
+ right_aligned_search: boolean
+ left_aligned_content: boolean
+ }
+
+ export type SocialPreferences = {
+ friend_privacy: FriendPrivacy
+ shared_instances_privacy: InvitePrivacy
+ hosting_access_privacy: InvitePrivacy
+ }
+
+ export type UserPreferences = {
+ appearance: AppearancePreferences
+ behavior: BehaviorPreferences
+ localization: LocalizationPreferences
+ layouts: LayoutPreferences
+ sidebars: SidebarPreferences
+ social: SocialPreferences
+ }
+
+ export type PartialUserPreferences = {
+ appearance?: Partial
+ behavior?: Partial
+ localization?: Partial
+ layouts?: Partial
+ sidebars?: Partial
+ social?: Partial
+ }
export type Pride26CampaignDonation = {
last_donated_at: string
diff --git a/packages/api-client/src/modules/labrinth/users/v3.ts b/packages/api-client/src/modules/labrinth/users/v3.ts
index 90dfced927..cc68ebc18f 100644
--- a/packages/api-client/src/modules/labrinth/users/v3.ts
+++ b/packages/api-client/src/modules/labrinth/users/v3.ts
@@ -37,6 +37,43 @@ export class LabrinthUsersV3Module extends AbstractModule {
)
}
+ /**
+ * Get a user's account preferences. The authenticated user may access their
+ * own preferences, while moderators may access another user's preferences.
+ *
+ * GET /v3/user/{id}/preferences
+ */
+ public async getPreferences(idOrUsername: string): Promise {
+ return this.client.request(
+ `/user/${encodeURIComponent(idOrUsername)}/preferences`,
+ {
+ api: 'labrinth',
+ version: 3,
+ method: 'GET',
+ },
+ )
+ }
+
+ /**
+ * Update a user's account preferences and return the fully resolved result.
+ *
+ * PATCH /v3/user/{id}/preferences
+ */
+ public async patchPreferences(
+ idOrUsername: string,
+ preferences: Labrinth.Users.v3.PartialUserPreferences,
+ ): Promise {
+ return this.client.request(
+ `/user/${encodeURIComponent(idOrUsername)}/preferences`,
+ {
+ api: 'labrinth',
+ version: 3,
+ method: 'PATCH',
+ body: preferences,
+ },
+ )
+ }
+
/**
* Search users by username prefix.
*
diff --git a/packages/app-lib/.sqlx/query-6f8b76b839db48539a70664af549c12be71df20b8cf520019413f6ea751cd594.json b/packages/app-lib/.sqlx/query-29df6a29d9221b9bbea45e024fb26f5b6a3155788880369af940fcd268d3cb94.json
similarity index 90%
rename from packages/app-lib/.sqlx/query-6f8b76b839db48539a70664af549c12be71df20b8cf520019413f6ea751cd594.json
rename to packages/app-lib/.sqlx/query-29df6a29d9221b9bbea45e024fb26f5b6a3155788880369af940fcd268d3cb94.json
index 21417b6d56..2b031b4401 100644
--- a/packages/app-lib/.sqlx/query-6f8b76b839db48539a70664af549c12be71df20b8cf520019413f6ea751cd594.json
+++ b/packages/app-lib/.sqlx/query-29df6a29d9221b9bbea45e024fb26f5b6a3155788880369af940fcd268d3cb94.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "\n SELECT\n max_concurrent_writes, max_concurrent_downloads,\n theme, locale, default_page, collapsed_navigation, hide_nametag_skins_page, advanced_rendering, native_decorations,\n discord_rpc, developer_mode, telemetry, personalized_ads,\n json(extra_launch_args) extra_launch_args, json(custom_env_vars) custom_env_vars,\n mc_memory_max, mc_force_fullscreen, mc_game_resolution_x, mc_game_resolution_y, hide_on_process_start,\n hook_pre_launch, hook_wrapper, hook_post_exit,\n custom_dir, prev_custom_dir, migrated, json(feature_flags) feature_flags, toggle_sidebar,\n skipped_update, pending_update_toast_for_version, auto_download_updates,\n version\n FROM settings\n ",
+ "query": "\n SELECT\n max_concurrent_writes, max_concurrent_downloads,\n theme, locale, default_page, collapsed_navigation, hide_nametag_skins_page, advanced_rendering, native_decorations,\n discord_rpc, developer_mode, telemetry, personalized_ads,\n json(extra_launch_args) extra_launch_args, json(custom_env_vars) custom_env_vars,\n mc_memory_max, mc_force_fullscreen, mc_game_resolution_x, mc_game_resolution_y, hide_on_process_start,\n hook_pre_launch, hook_wrapper, hook_post_exit,\n custom_dir, prev_custom_dir, migrated, json(feature_flags) feature_flags, toggle_sidebar,\n skipped_update, pending_update_toast_for_version, auto_download_updates,\n sync_theme_across_devices, sync_behavior_across_devices,\n version\n FROM settings\n ",
"describe": {
"columns": [
{
@@ -159,9 +159,19 @@
"type_info": "Integer"
},
{
- "name": "version",
+ "name": "sync_theme_across_devices",
"ordinal": 31,
"type_info": "Integer"
+ },
+ {
+ "name": "sync_behavior_across_devices",
+ "ordinal": 32,
+ "type_info": "Integer"
+ },
+ {
+ "name": "version",
+ "ordinal": 33,
+ "type_info": "Integer"
}
],
"parameters": {
@@ -199,8 +209,10 @@
true,
true,
true,
+ false,
+ false,
false
]
},
- "hash": "6f8b76b839db48539a70664af549c12be71df20b8cf520019413f6ea751cd594"
+ "hash": "29df6a29d9221b9bbea45e024fb26f5b6a3155788880369af940fcd268d3cb94"
}
diff --git a/packages/app-lib/.sqlx/query-dfdc99cb5ec60bb08c566920628deec7e741eca9490811ebad6594abe8a6126d.json b/packages/app-lib/.sqlx/query-88c6a22aa112f10dce9d42779f06c723b356263ffc83aabb645c9fb5b6d5aa7c.json
similarity index 85%
rename from packages/app-lib/.sqlx/query-dfdc99cb5ec60bb08c566920628deec7e741eca9490811ebad6594abe8a6126d.json
rename to packages/app-lib/.sqlx/query-88c6a22aa112f10dce9d42779f06c723b356263ffc83aabb645c9fb5b6d5aa7c.json
index e4cd418a9e..fe54b8050f 100644
--- a/packages/app-lib/.sqlx/query-dfdc99cb5ec60bb08c566920628deec7e741eca9490811ebad6594abe8a6126d.json
+++ b/packages/app-lib/.sqlx/query-88c6a22aa112f10dce9d42779f06c723b356263ffc83aabb645c9fb5b6d5aa7c.json
@@ -1,12 +1,12 @@
{
"db_name": "SQLite",
- "query": "\n UPDATE settings\n SET\n max_concurrent_writes = $1,\n max_concurrent_downloads = $2,\n\n theme = $3,\n locale = $4,\n default_page = $5,\n collapsed_navigation = $6,\n advanced_rendering = $7,\n native_decorations = $8,\n\n discord_rpc = $9,\n developer_mode = $10,\n telemetry = $11,\n personalized_ads = $12,\n\n extra_launch_args = jsonb($13),\n custom_env_vars = jsonb($14),\n mc_memory_max = $15,\n mc_force_fullscreen = $16,\n mc_game_resolution_x = $17,\n mc_game_resolution_y = $18,\n hide_on_process_start = $19,\n\n hook_pre_launch = $20,\n hook_wrapper = $21,\n hook_post_exit = $22,\n\n custom_dir = $23,\n prev_custom_dir = $24,\n migrated = $25,\n\n toggle_sidebar = $26,\n feature_flags = $27,\n hide_nametag_skins_page = $28,\n\n skipped_update = $29,\n pending_update_toast_for_version = $30,\n auto_download_updates = $31,\n\n version = $32\n ",
+ "query": "\n UPDATE settings\n SET\n max_concurrent_writes = $1,\n max_concurrent_downloads = $2,\n\n theme = $3,\n locale = $4,\n default_page = $5,\n collapsed_navigation = $6,\n advanced_rendering = $7,\n native_decorations = $8,\n\n discord_rpc = $9,\n developer_mode = $10,\n telemetry = $11,\n personalized_ads = $12,\n\n extra_launch_args = jsonb($13),\n custom_env_vars = jsonb($14),\n mc_memory_max = $15,\n mc_force_fullscreen = $16,\n mc_game_resolution_x = $17,\n mc_game_resolution_y = $18,\n hide_on_process_start = $19,\n\n hook_pre_launch = $20,\n hook_wrapper = $21,\n hook_post_exit = $22,\n\n custom_dir = $23,\n prev_custom_dir = $24,\n migrated = $25,\n\n toggle_sidebar = $26,\n feature_flags = $27,\n hide_nametag_skins_page = $28,\n\n skipped_update = $29,\n pending_update_toast_for_version = $30,\n auto_download_updates = $31,\n\n sync_theme_across_devices = $32,\n sync_behavior_across_devices = $33,\n\n version = $34\n ",
"describe": {
"columns": [],
"parameters": {
- "Right": 32
+ "Right": 34
},
"nullable": []
},
- "hash": "dfdc99cb5ec60bb08c566920628deec7e741eca9490811ebad6594abe8a6126d"
+ "hash": "88c6a22aa112f10dce9d42779f06c723b356263ffc83aabb645c9fb5b6d5aa7c"
}
diff --git a/packages/app-lib/migrations/20260818120000_sync-preferences-settings.sql b/packages/app-lib/migrations/20260818120000_sync-preferences-settings.sql
new file mode 100644
index 0000000000..f97419c46b
--- /dev/null
+++ b/packages/app-lib/migrations/20260818120000_sync-preferences-settings.sql
@@ -0,0 +1,5 @@
+ALTER TABLE settings
+ADD COLUMN sync_theme_across_devices INTEGER NOT NULL DEFAULT TRUE;
+
+ALTER TABLE settings
+ADD COLUMN sync_behavior_across_devices INTEGER NOT NULL DEFAULT TRUE;
diff --git a/packages/app-lib/src/api/users.rs b/packages/app-lib/src/api/users.rs
index aa4f1d60ec..8f6f71d0d8 100644
--- a/packages/app-lib/src/api/users.rs
+++ b/packages/app-lib/src/api/users.rs
@@ -109,6 +109,51 @@ pub async fn get_user_collections(user_id: &str) -> crate::Result {
.await
}
+#[tracing::instrument]
+pub async fn get_user_preferences(user_id: &str) -> crate::Result {
+ let state = State::get().await?;
+ let user_id = urlencoding::encode(user_id);
+
+ fetch_json(
+ Method::GET,
+ &format!(
+ "{}user/{}/preferences",
+ env!("MODRINTH_API_URL_V3"),
+ user_id
+ ),
+ None,
+ None,
+ Some("/v3/user/:id/preferences"),
+ &state.api_semaphore,
+ &state.pool,
+ )
+ .await
+}
+
+#[tracing::instrument(skip(preferences))]
+pub async fn patch_user_preferences(
+ user_id: &str,
+ preferences: Value,
+) -> crate::Result {
+ let state = State::get().await?;
+ let user_id = urlencoding::encode(user_id);
+
+ fetch_json(
+ Method::PATCH,
+ &format!(
+ "{}user/{}/preferences",
+ env!("MODRINTH_API_URL_V3"),
+ user_id
+ ),
+ None,
+ Some(preferences),
+ Some("/v3/user/:id/preferences"),
+ &state.api_semaphore,
+ &state.pool,
+ )
+ .await
+}
+
#[tracing::instrument(skip(patch))]
pub async fn patch_user(user_id: &str, patch: Value) -> crate::Result<()> {
let state = State::get().await?;
diff --git a/packages/app-lib/src/state/settings.rs b/packages/app-lib/src/state/settings.rs
index 6f64fc7108..a8bb8fe92e 100644
--- a/packages/app-lib/src/state/settings.rs
+++ b/packages/app-lib/src/state/settings.rs
@@ -19,6 +19,8 @@ pub struct Settings {
pub advanced_rendering: bool,
pub native_decorations: bool,
pub toggle_sidebar: bool,
+ pub sync_theme_across_devices: bool,
+ pub sync_behavior_across_devices: bool,
pub telemetry: bool,
pub discord_rpc: bool,
@@ -88,6 +90,7 @@ impl Settings {
hook_pre_launch, hook_wrapper, hook_post_exit,
custom_dir, prev_custom_dir, migrated, json(feature_flags) feature_flags, toggle_sidebar,
skipped_update, pending_update_toast_for_version, auto_download_updates,
+ sync_theme_across_devices, sync_behavior_across_devices,
version
FROM settings
"
@@ -146,6 +149,8 @@ impl Settings {
pending_update_toast_for_version: res
.pending_update_toast_for_version,
auto_download_updates: res.auto_download_updates.map(|x| x == 1),
+ sync_theme_across_devices: res.sync_theme_across_devices == 1,
+ sync_behavior_across_devices: res.sync_behavior_across_devices == 1,
version: res.version as usize,
})
}
@@ -206,7 +211,10 @@ impl Settings {
pending_update_toast_for_version = $30,
auto_download_updates = $31,
- version = $32
+ sync_theme_across_devices = $32,
+ sync_behavior_across_devices = $33,
+
+ version = $34
",
max_concurrent_writes,
max_concurrent_downloads,
@@ -239,6 +247,8 @@ impl Settings {
self.skipped_update,
self.pending_update_toast_for_version,
self.auto_download_updates,
+ self.sync_theme_across_devices,
+ self.sync_behavior_across_devices,
version,
)
.execute(exec)
diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts
index 6d99b45fcb..4b2ddfcd39 100644
--- a/packages/ui/src/components/index.ts
+++ b/packages/ui/src/components/index.ts
@@ -13,7 +13,6 @@ export * from './page'
export * from './project'
export * from './search'
export * from './servers'
-export * from './settings'
export * from './sharing'
export * from './skin'
export * from './user'
diff --git a/packages/ui/src/components/modal/TabbedModal.vue b/packages/ui/src/components/modal/TabbedModal.vue
index 7dfc1847a8..04f4291487 100644
--- a/packages/ui/src/components/modal/TabbedModal.vue
+++ b/packages/ui/src/components/modal/TabbedModal.vue
@@ -2,10 +2,11 @@
-
-
-
-
-
-
-
- {{
- isQueryEmpty()
- ? ''
- : formatMessage(messages.searchResultsAnnouncement, {
- matches: $searchResults.get('searchResult')?.length ?? 0,
- })
- }}
-
-
-
-
-
-
- {{ getCategoryName(category) }}
-
-
-
- {{ formatMessage(messages.noResults) }}
-
-
-
- onItemClick(e, loc)"
- @keydown="(e) => onItemKeydown(e, loc)"
- >
-
-
-
-
-
- {{ loc.displayName }}
-
-
-
- {{ loc.translatedName }}
-
-
-
-
-
-
-
-
diff --git a/packages/ui/src/components/settings/index.ts b/packages/ui/src/components/settings/index.ts
deleted file mode 100644
index c7efceb5dc..0000000000
--- a/packages/ui/src/components/settings/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as LanguageSelector } from './LanguageSelector.vue'
-export { default as ThemeSelector } from './ThemeSelector.vue'
diff --git a/packages/ui/src/layouts/index.ts b/packages/ui/src/layouts/index.ts
index d027ea875e..e2cc22785e 100644
--- a/packages/ui/src/layouts/index.ts
+++ b/packages/ui/src/layouts/index.ts
@@ -1,3 +1,4 @@
+export * from './shared/appearance-settings'
export * from './shared/browse-tab'
export * from './shared/console'
export * from './shared/content-tab'
diff --git a/packages/ui/src/layouts/shared/appearance-settings/components/AppearanceSettingRow.vue b/packages/ui/src/layouts/shared/appearance-settings/components/AppearanceSettingRow.vue
new file mode 100644
index 0000000000..1abded2318
--- /dev/null
+++ b/packages/ui/src/layouts/shared/appearance-settings/components/AppearanceSettingRow.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+ {{ title }}
+
+
+ {{ description }}
+
+
+
+
+
+
+
diff --git a/packages/ui/src/layouts/shared/appearance-settings/components/ProjectLayoutSelector.vue b/packages/ui/src/layouts/shared/appearance-settings/components/ProjectLayoutSelector.vue
new file mode 100644
index 0000000000..59a6fa0b1b
--- /dev/null
+++ b/packages/ui/src/layouts/shared/appearance-settings/components/ProjectLayoutSelector.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
diff --git a/packages/ui/src/layouts/shared/appearance-settings/components/appearance-settings-theme-selector.stories.ts b/packages/ui/src/layouts/shared/appearance-settings/components/appearance-settings-theme-selector.stories.ts
new file mode 100644
index 0000000000..69a404bb83
--- /dev/null
+++ b/packages/ui/src/layouts/shared/appearance-settings/components/appearance-settings-theme-selector.stories.ts
@@ -0,0 +1,31 @@
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+import { ref } from 'vue'
+
+import AppearanceSettingsThemeSelector from './appearance-settings-theme-selector.vue'
+
+const meta = {
+ title: 'Settings/ThemeSelector',
+ // @ts-ignore - error comes from generically typed component
+ component: AppearanceSettingsThemeSelector,
+} satisfies Meta
+
+export default meta
+
+export const Interactive: StoryObj = {
+ render: () => ({
+ components: { AppearanceSettingsThemeSelector },
+ setup() {
+ const currentTheme = ref('dark')
+ const themeOptions = ['system', 'light', 'dark', 'oled', 'retro']
+ return { currentTheme, themeOptions }
+ },
+ template: `
+
+ `,
+ }),
+}
diff --git a/packages/ui/src/components/settings/ThemeSelector.vue b/packages/ui/src/layouts/shared/appearance-settings/components/appearance-settings-theme-selector.vue
similarity index 67%
rename from packages/ui/src/components/settings/ThemeSelector.vue
rename to packages/ui/src/layouts/shared/appearance-settings/components/appearance-settings-theme-selector.vue
index 80ec1be2dc..639dc0df29 100644
--- a/packages/ui/src/components/settings/ThemeSelector.vue
+++ b/packages/ui/src/layouts/shared/appearance-settings/components/appearance-settings-theme-selector.vue
@@ -1,25 +1,22 @@
-
+