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" >
-

+

{{ 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 @@