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 <mr.trumgao@gmail.com>
This commit is contained in:
Calum H.
2026-08-20 07:23:01 +00:00
committed by GitHub
co-authored by tdgao
parent 79fcf41316
commit 3e07267e10
191 changed files with 3356 additions and 4372 deletions
@@ -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<InstallJobSnapshot[]>([])
const iconUrls = ref<Record<string, string | null>>({})
const instanceNames = ref<Record<string, string>>({})
@@ -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 {
@@ -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<FeatureFlag, boolean>
const syncBehaviorAcrossDevices = ref(false)
const featureFlags = reactive<FeatureFlags>({ ...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
}
@@ -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)
}
@@ -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<ColorTheme, 'system'>
const preferred = ref<ColorTheme>('dark')
const preview = ref<ColorTheme | null>(null)
const advancedRendering = ref(true)
const syncAcrossDevices = ref(false)
const nativeThemeQuery = window.matchMedia('(prefers-color-scheme: dark)')
const native = ref<Theme>(nativeThemeQuery.matches ? 'dark' : 'light')
const active = computed<Theme>(() => {
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
}